利用TCP协议服务器从单用户到多用户的理解思路与解决办法(selectpollepoll)(一)

利用TCP协议服务器从单用户到多用户的理解思路与解决办法(selectpollepoll)(一),第1张

在进行TCP协议的了解之前,首先要了解用到的基本函数:

**socket函数是一种可用于根据指定的地址族数据类型协议来分配一个套接口的描述字及其所用的资源的函数

int socket(int Adress_family,int type,int protocol);

af:如AF_INET   type:连接类型,通常是SOCK_STREAM或SOCK_DGRAM    protocol:协议类型,通常是IPPROTO_TCP或IPPROTO_UDP 返回值socket的编号,为-1表示失败

** 将一个地址和一个端口号绑定到一个socket连接上

  int bind(int socket,sockaddr * address,uint addrlen);

socket:之前创建的socket, sockaddr:一个用来存放Ip地址和端口号的结构体,addrlen:此结构体的长度  返回值:为-1表示失败,若端口被占用,会从新绑定一个随机端口(仍返回失败)

**// UDP时:接收任何一个发送到该socket的消息(无法获取发送方地址)

**// TCP时:接收一个已连接的socket (connected socket)发送的信息

int recv(int socket,char * buf,uint buflen,int flag);【阻塞】

socket:UDP时,为之前创建的socket,TCP时,为connected socket,buf:接收的缓冲区,buflen:缓冲区的长度,flag:一般为0    返回值:>0表示收到的字节数,=0表示连接被关闭,-1表示出错

  • 注意:对于TCP,请确保socket是已连接的,因为只有已连接的socket会阻塞此函数
  • 该函数实际上是从缓冲区取指定长度的数据,如果缓冲区没有数据,则会阻塞;如果没有取完,则下次使用此函数的时候不会阻塞
  • 应注意:当一次无法获得对方发送的全部数据,在数据不完整的时候,程序可能无法向下执行,可以考虑将数据放在缓冲区中,等数据全部接收完成的时候再使用

 **将一个socket设置为监听状态,专门用来监听的socket叫做master socket

int listen(int socket,int maxconn);【仅TCP】【服务器端】

 maxconn:最大接收连接数, 返回值:失败返回-1,成功返回0

**接收一个客户机的连接,返回一个socket,来自客户机的socket叫connected socket

int accept(int socket,sockaddr * fromaddr,int * addrlen);【阻塞】【仅TCP】【服务器】

socket:用来监听的socket(master socket),fromaddr:客户机的地址信息, addrlen:地址结构体的长度(输入输出参数) 返回值:返回一个新的socket,这个socket专门用来与此客户机通讯(connected socket)    

**使用当前socket连接一个地址(与服务器建立正式连接),此函数会触发服务器端的accept、select函数

int connect(int socket,sockaddr * addr,int addrlen);【仅TCP】【客户端】

 返回值:成功则返回0,失败返回非0,错误码GetLastError()。


**向一个已连接的socket发送信息,这个socket应该是connected socket(非master socket)

int send(int socket,char * buf,char buflen,int flag);【仅TCP】

**关闭一个已存在的socket【正常关闭】

int closesocket(int socket);

返回值:失败返回-1,成功返回0

初识TCP协议链接(单用户):

       在认识tcpserver之前,简单理解服务端和客户端是如何交流的,如下:

       listenfd看成是服务端的“迎宾”,负责把对应的输入带进来;

       connld看成是负责对输入的进行 *** 作的“服务员”;

       并且要理解如listen,bind等 *** 作是利用内核的 *** 作,分配出资源等待连接,故只用进行一次即可。


       对此结构图,利用基本的函数构建了第一个简单的tcpserver.c:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char **argv) 
{
    int listenfd, connfd, n; 
    struct sockaddr_in servaddr;
    
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }  //这里创建一个迎宾,每来一个客户端,socket的正常输出+1:eg先是2,再来一个就会分配3

    memset(&servaddr, 0, sizeof(servaddr));/*将指针变量servaddr所指向的前sizeof(servaddr)字
节的内存单元用一个“整数” 0 替换,注意0是 int 型。


&servaddr是 void* 型的指针变量,所以它可以为任 何类型的数据进行初始化。


*/ servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示所有网卡 servaddr.sin_port = htons(9999); if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) { printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno); return 0; } //迎宾绑定端口 if (listen(listenfd, 10) == -1) { printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno); return 0; }//迎宾监听端口 struct sockaddr_in client; socklen_t len = sizeof(client); if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) { printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno); return 0; } //这里就是迎宾把任务带过来,判断是不是出错 printf("========waiting for client's request========\n"); while (1) { n = recv(connfd, buff, MAXLNE, 0); if (n > 0) { buff[n] = '\0'; printf("recv msg from client: %s\n", buff); send(connfd, buff, n, 0); } else if (n == 0) { close(connfd); } close(connfd); } close(listenfd); return 0; }

        在对其进行编译生成可执行文件,发现我们最后只能成功得到一个客户端发送的信息,并在服务器端进行 *** 作。


        若第二个客户端对其进行连接请求,请求先在内核里的协议栈里进行三次握手,实现连接,但是服务器应用层并没有关系。


        那么为了对更多用户机进行 *** 作,讨论了来一个客户端分配一个线程的思路:

利用线程实现多用户(用户少约C10K):

        于是将处理的while(1)的优化成如下代码,并创建了一个函数,去把之前的whlie里面的任务放进去,分配一个线程。


但是没分配一个线程,内存的使用量也会增加,当超过最大使用内存时,会造成服务器重启。


void *client_routine(void *arg) { //

	int connfd = *(int *)arg;
	char buff[MAXLNE];
	while (1) {
		int n = recv(connfd, buff, MAXLNE, 0);
        if (n > 0) {
            buff[n] = '\0';
            printf("recv msg from client: %s\n", buff);
	    	send(connfd, buff, n, 0);
        } else if (n == 0) {
            close(connfd);
			break;
        }
	}
	return NULL;
}	

while (1) {
		struct sockaddr_in client;
	    socklen_t len = sizeof(client);
	    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
	        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
	        return 0;
	    }//这里可以理解为,每一桌分配一个不一样的服务员connfd,以便区分是拿一个客户端发送的信息;
         //但是,没有合适的组件去查找,到底是具体的客户端发送的信息
		pthread_t threadid;  //unsigned long int
		pthread_create(&threadid, NULL, client_routine, (void*)&connfd);//来一个分配一个
    }

        除此之外,我们可以发现,每当我们accept一个client,我们就分配一个不同的服务员,这样不利于我们对每一个客户端进行准确服务(这里的理解不是很准确),于是我们可以利用一个特殊组件:select;

利用(select组件)实现多用户(一个selectC10K,与线程复用selectC1000K ):

        select能够同时监控输入rset和输出wset,通过对客户端fd的管理,能够是一次能够处理的客户端数量增加很多,达到一个更好的服务数量。


	fd_set rfds, rset, wfds, wset;//实际上就是我们监听的一个个集合(bool数组)

	FD_ZERO(&rfds);//清空所有fd,初始化为0
	FD_SET(listenfd, &rfds);

	FD_ZERO(&wfds);

	int max_fd = listenfd;

	while (1) {

		rset = rfds;
		wset = wfds;
		int nready = select(max_fd+1, &rset, &wset, NULL, NULL);
        //max_fd+1是之后set中的最大fd容量加1
//int nfds,  fd_set* readset,  fd_set* writeset,  fe_set* exceptset,  struct timeval* timeout
		if (FD_ISSET(listenfd, &rset)) { //可读为非0
            //如果这次listenfd不存在,则放进来(实则就是有新客户端连接)
			struct sockaddr_in client;
		    socklen_t len = sizeof(client);
		    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
		        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
		        return 0;
		    }

			FD_SET(connfd, &rfds);
			if (connfd > max_fd) max_fd = connfd;
			if (--nready == 0) continue;
		}
		int i = 0;
		for (i = listenfd+1;i <= max_fd;i ++) {

			if (FD_ISSET(i, &rset)) { // 

				n = recv(i, buff, MAXLNE, 0);
		        if (n > 0) {
		            buff[n] = '\0';
		            printf("recv msg from client: %s\n", buff);
					FD_SET(i, &wfds);
					//reactor
					//send(i, buff, n, 0);
		        } else if (n == 0) { //当客户端调用close的时候返回0

					FD_CLR(i, &rfds);//清除fd,
					//printf("disconnect\n");
		            close(i);
					
		        }
				if (--nready == 0) break;
			} else if (FD_ISSET(i, &wset)) {
				send(i, buff, n, 0);
				FD_SET(i, &rfds);		
			}
		}
	}

        这里的select有上限主要是因为每执行一次select的时候,rset和wset都要被拿到内核里面去处理,对于大量fd管理,还是有一定局限性。


利用(poll组件)实现多用户:

        poll相当于每隔一段时间去检查客户端状态的结构体的集合。


        
    struct pollfd fds[POLL_SIZE] = {0};
	fds[0].fd = listenfd;
	fds[0].events = POLLIN;

	int max_fd = listenfd;
	int i = 0;
	for (i = 1;i < POLL_SIZE;i ++) {
		fds[i].fd = -1;
	}

	while (1) {

		int nready = poll(fds, max_fd+1, -1);
	//fds是结构体pollfd的数组,nfds用来标记结构体的总数,是poll函数调用阻塞的时间,单位:毫秒;
		if (fds[0].revents & POLLIN) {

			struct sockaddr_in client;
		    socklen_t len = sizeof(client);
		    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
		        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
		        return 0;
		    }

			printf("accept \n");
			fds[connfd].fd = connfd;
			fds[connfd].events = POLLIN;

			if (connfd > max_fd) max_fd = connfd;

			if (--nready == 0) continue;
		}

		//int i = 0;
		for (i = listenfd+1;i <= max_fd;i ++)  {

			if (fds[i].revents & POLLIN) {
				
				n = recv(i, buff, MAXLNE, 0);
		        if (n > 0) {
		            buff[n] = '\0';
		            printf("recv msg from client: %s\n", buff);

					send(i, buff, n, 0);
		        } else if (n == 0) { //

					fds[i].fd = -1; //清空fds

		            close(i);
					
		        }
				if (--nready == 0) break;
			}
		}
	}
利用(epoll组件)实现多用户:

        epoll是由poll、select的一个函数改成了三个函数:

                epoll_create:快递员  epoll_ctr(ADD,DEL,MOD):搬出搬进用户  epoll_wait:

        epoll\select\poll都是事件驱动,只是管理的方式不同而已。



	int epfd = epoll_create(1); //这里的参数大于0就行

	struct epoll_event events[POLL_SIZE] = {0}; //设置快递员的袋子大小
	struct epoll_event ev;

	ev.events = EPOLLIN;
	ev.data.fd = listenfd;

	epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);

	while (1) {

		int nready = epoll_wait(epfd, events, POLL_SIZE, 5);
		if (nready == -1) {
			continue;
		}

		int i = 0;
		for (i = 0;i < nready;i ++) {

			int clientfd =  events[i].data.fd;
			if (clientfd == listenfd) {

				struct sockaddr_in client;
			    socklen_t len = sizeof(client);
			    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
			        printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
			        return 0;
			    }

				printf("accept\n");
				ev.events = EPOLLIN;
				ev.data.fd = connfd;
				epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);

			} else if (events[i].events & EPOLLIN) {//判断是否进行读的操作

				printf("recv\n");
				n = recv(clientfd, buff, MAXLNE, 0);
		        if (n > 0) {
		            buff[n] = '\0';
		            printf("recv msg from client: %s\n", buff);

					send(clientfd, buff, n, 0);
		        } else if (n == 0) { //清楚 *** 作


					ev.events = EPOLLIN;
					ev.data.fd = clientfd;

					epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);

		            close(clientfd);				
		        }
			}
		}
	}
	

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/563542.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-02
下一篇 2022-04-02

发表评论

登录后才能评论

评论列表(0条)

保存