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

在进行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表示失败

 观察源码:

int __sys_socket(int family, int type, int protocol)
{
	struct socket *sock;
	int flags;

	sock = __sys_socket_create(family, type, protocol);
	if (IS_ERR(sock))
		return PTR_ERR(sock);

	flags = type & ~SOCK_TYPE_MASK;
	if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
		flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;

	return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}

----------创建socket对象,这里不进一步深入底层,可以自行查找源码
static struct socket *__sys_socket_create(int family, int type, int protocol)
{
	struct socket *sock;
	int retval;

	/* Check the SOCK_* constants for consistency.  */
	BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
	BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
	BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
	BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);

	if ((type & ~SOCK_TYPE_MASK) & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
		return ERR_PTR(-EINVAL);
	type &= SOCK_TYPE_MASK;

	retval = sock_create(family, type, protocol, &sock);
	if (retval < 0)
		return ERR_PTR(retval);

	return sock;
}

----------对应的socket对象如下:
/**
 *  struct socket - general BSD socket
 *  @state: socket state (%SS_CONNECTED, etc)
 *  @type: socket type (%SOCK_STREAM, etc)
 *  @flags: socket flags (%SOCK_NOSPACE, etc)
 *  @ops: protocol specific socket operations
 *  @file: File back pointer for gc
 *  @sk: internal networking protocol agnostic socket representation
 *  @wq: wait queue for several uses
 */

struct socket {//socket对象是绑定给文件的属性,返回的fd是我们来了解到对应文件的手段
	socket_state		state;

	short			type;

	unsigned long		flags;

	struct file		*file;
	struct sock		*sk;
	const struct proto_ops	*ops;

	struct socket_wq	wq;
};

补充:socket_state--->
LISTEN - 侦听来自远方TCP端口的连接请求; 

SYN-SENT -在发送连接请求后等待匹配的连接请求; 

SYN-RECEIVED- 在收到和发送一个连接请求后等待对连接请求的确认; 

ESTABLISHED- 代表一个打开的连接,数据可以传送给用户; 

FIN-WAIT-1 - 等待远程TCP的连接中断请求,或先前的连接中断请求的确认;

FIN-WAIT-2 - 从远程TCP等待连接中断请求; 

CLOSE-WAIT - 等待从本地用户发来的连接中断请求; 

CLOSING -等待远程TCP对连接中断的确认; 

LAST-ACK - 等待原来发向远程TCP的连接中断请求的确认; 

TIME-WAIT -等待足够的时间以确保远程TCP接收到连接中断请求的确认; 

CLOSED - 没有任何连接状态;

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

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

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

int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
{
	struct socket *sock;
	struct sockaddr_storage address;
	int err, fput_needed;//拿出内部的属性

	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	if (sock) {
		err = move_addr_to_kernel(umyaddr, addrlen, &address);//用户空间转移到内核空间,再进行操作
		if (!err) {
			err = security_socket_bind(sock,
						   (struct sockaddr *)&address,
						   addrlen);
			if (!err)
				err = sock->ops->bind(sock,
						      (struct sockaddr *)
						      &address, addrlen);
		}
		fput_light(sock->file, fput_needed);
	}
	return err;
}

**// 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

int __sys_listen(int fd, int backlog)
{
	struct socket *sock;
	int err, fput_needed;
	int somaxconn;

	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	if (sock) {
		somaxconn = READ_ONCE(sock_net(sock->sk)->core.sysctl_somaxconn);
		if ((unsigned int)backlog > somaxconn)
			backlog = somaxconn; //不能超过系统限制

		err = security_socket_listen(sock, backlog);
		if (!err)
			err = sock->ops->listen(sock, backlog);

		fput_light(sock->file, fput_needed);
	}
	return err;
}

**接收一个客户机的连接,返回一个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

 **关闭读写。它将防止套接字上更多的数据的读写。(没关闭socket)

int close(int sockfd);

返回值:成功则返回0,错误返回-1,错误码errno

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

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

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

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

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

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

#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/poll.h>
#include <sys/epoll.h>
#include <pthread.h>

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;
}

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

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

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

利用线程BIO是同步阻塞,多线程多请求)实现多用户(用户少约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,以便区分是拿一个客户端发送的信息;
         //但是,没有合适的组件去查找,到底是具体的客户端发送的信息
         //accept是阻塞的,不受到信息不会继续。
		pthread_t threadid;  //unsigned long int
		pthread_create(&threadid, NULL, client_routine, (void*)&connfd);
        //这里是来一个分配一个线程进行什么操作,不会进入函数本身,不会阻塞在这里
    }

       思考一个问题,while(1)里面存在一个阻塞,程序会正常运行吗?

       在这里是可以的,线程操作不是阻塞的,也就是说,while(1)里面只有accept是阻塞的,函数只会停留在accept等待通过传输层TCP连接进来的客户端信息,不影响很多客户端的连接,故能正常运行。

        分配线程去实现多用户连接的很大一个缺陷就是一线程一客户端,我们可以向一个线程里面装很多个fd数组,遍历他们实现数据传输,实现NIO(NIO是同步非阻塞,单线程多请求),最大化实现硬件利用最大化。但是这会造成对内核的多次读取,造成延时,那么有没有新的io组件优化这个过程呢?

        select组件(复用了一个线程)能使读取的往返只需要1次。

利用(select组件)(原始IO多路复用)实现多用户(一个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管理,还是有一定局限性。

select和poll的本质区别是不大的。所以他们有相同的弊端:

1、一次需要传递一个fds数组过去,可能很大 (那么为什么不动态的传递、删除fds呢?)————————————epoll

2、内核需要再去遍历这个fd数组  (那为什么不用计算机中断处理fdevent呢?)

————————————epoll

利用(poll组件)(原始IO多路复用)实现多用户:

        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组件)IO多路转接技术)(IO多路复用进阶版)实现多用户:

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

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

具体如下:

epoll_create:创建一个epoll的句柄,返回该对象的描述符,注意要使用 close 关闭该描述符。

int epoll_create(int size); //从 Linux 内核 2.6.8 版本起,只要求 size 大于 0 即可。

epoll_ctl:操作控制 epoll 对象,主要涉及 epoll 红黑树上节点的一些操作,比如添加节点,删除节点,修改节点事件

 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//epfd为对应句柄

epoll_wait:阻塞一段时间并等待事件发生,返回事件集合,也就是获取内核的事件通知。说白了就是遍历双向链表,把双向链表里的节点数据拷贝出来,拷贝完毕后就从双向链表移除。

 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

//timeout:阻塞等待的时间长短,以毫秒为单位,如果传入 -1 代表永久阻塞。

//maxevents: 从epoll的就绪队列中赋值最多50个节点到用户态进行处理

与select和poll相比,存储管理fd的方式很不同:

        1、select的数据结构是数组,poll的数据结构是链表;epoll是红黑树;

        显而易见,epoll的查找和插入速度综合起来是更快的

        2、epoll为每一个epfd创建一个用户缓冲区(数据结构是链表),内核epoll结构只需要把有数据的fd放到缓冲区;(这意味着不会将整个fd结构进行遍历)

    #define POLL_SIZE	1024

	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);//把listenfd放进epoll管理的fd组里

	while (1) {

		int nready = epoll_wait(epfd, events, POLL_SIZE, 5);
        //返回这段时间内,多少fd有事件(所有类型fd在一起)
		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);				
		        }
			}
		}
	}
	

epoll优势:

不用每次都传递一个完整的fd数组过去,而是通过events触发fd(在这里我们可以把fd、io和socket当成同一个东西)的操作简化了读取操作,优于select和poll

epoll弊端:

        暂且留到栏目二讨论

(C++)利用TCP协议服务器(epoll),实现reactor(二)--------------------附直接实现代码_Gpangpangwa的博客-CSDN博客

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值