.为什么单线程能接受多个连接但是不能传输数据?
因为listenfd处于listen状态,三次握手是在协议栈完成的,不受应用程序控制。三次连接不发生在任意函数中,是协议栈自动完成的!
一请求一线程
2.如何解决多个客户端的连接问题:
一个请求一个线程
逻辑简单,不适合高并发。一个线程栈8M,难以突破C10k。
普通的网络io当中,基本上是一请求一回应
下面是一请求一回应
// 请求与响应
int main(int argc, char **argv)
{
//以下服务端的处理
int listenfd, connfd, n; //创建fd
struct sockaddr_in servaddr;
char buff[MAXLNE];
// 利用socket函数创建套接字
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
// 初始化
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(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;
}
//调用listen函数将套接字转为可接收的状态
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);
//调用accept函数接受连接请求
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);
}
close(connfd);
}
}//以上这个是,一个客户端和一个服务端的交互
以上这个是,一个客户端和一个服务端的交互,有了这个我们再去思考一个问题就是,如何进行多个客户端交互呢
我们先想了个办法,我们把
struct sockaddr_in client;
socklen_t len = sizeof(client);
//调用accept函数接受连接请求
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
这部分代码复制到while循环里
printf("========waiting for client's request========\n");
while (1) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
//调用accept函数接受连接请求
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
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);
}
close(connfd);
}
我们就会发现,虽然能连接多个客户端,但是客户端不能持续地发送消息,只能发送一条。
所以这种方式只解决了多个客户端连接的问题,并不能正常工作
因为一个客户端一连接,就会阻塞在accept这里,accept一返回,然后往下走,send也会阻塞。所以只会多个连接,每个客户端只会发一条
在这里,我们又想到多线程,一个连接分配一个线程,这样可行吗
我们继续改一下
printf("========waiting for client's request========\n");
while (1) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
//调用accept函数接受连接请求
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
pthread_t threadid;
pthread_create(&threadid, NULL, client_routine, (void*)&connfd);
下边的代码是线程的设计
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;
}//子线程做一个接收和发送
这段代码是接收多个客户端多个数据,这个代码就实现了多个客户端和多个数据
但是这个方法有局限性,限于客户端不太多,数据比较少的情况下可以这么做,数量一大就有影响了
按照8M大小的线程来算的话,4个g的内存,那么就是512个线程
随着客户端数量越来越多,那么你的内存空间会越来越满,导致系统可能重启,很难突破一个数量,所以很难实现万级连接
在以上的场景中,我们会阻塞在accept和recv这两个场景
我们就要想一个办法,去分清fd,能够知道fd是哪个客户端发送的,哪个fd是需要处理的,理清楚哪个连接是出问题的,把若干个fd,放到一个组件中集中去处理
我们这里引入一个io多路复用的组件,叫select
一个客户端连接进来都有一个fd,选择需要处理的fd,首先我们需要一个fd的集合
fd_set rfds;//fd_set通过一个bit_set,一个fd进来后,如果有事件需要处理,就把这个bit位置为1,bit的下标对应fd的值。
fd_set有多大呢,1024/(8*sizeof(long))
fd_set rfds, rset, wfds, wset;// fd的集合,bit_set
FD_ZERO(&rfds);//先清空fd集合
FD_SET(listenfd, &rfds);//监控listenfd是需要处理的,是有数据的是可读的
FD_ZERO(&wfds);//清空可写的集合
int max_fd = listenfd;//需要select轮询的fd的最大值,也相当于公司员工id的最大值
while (1) {
rset = rfds;
wset = wfds;
int nready = select(max_fd+1, &rset, &wset, NULL, NULL);//监控是否可读可写,第二个三个四个参数是fd的属性,第五个参数就是多长时间轮询一次 ,select函数本身一个阻塞的,如果监控的集合里边没有符合要求(可读的)的,如果是null则会一直阻塞,如果传进去一个值就是
if (FD_ISSET(listenfd, &rset)) { //如果是listen的fd
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 ++) {// 从listen的后一位开始
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);这里用send也可以,因为buff没有满
} else if (n == 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函数
int select (int maxfd + 1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval * timeout);最大的特点是带五个参数,分别传入要监听读、写、错误事件的fd_set,并返回对应位是否有事件。maxfd + 1传给内核,内核根据maxfd + 1遍历所有fd,如果缓冲区有数据可读且注册了改fd的读事件就将readset对应位置1后返回给用户。挨个检测fd, fd有数据可读,就置可读事件。 fd的sendbuffer为空,就置为可写。
使用FD_SET(socket,&fds)就是把对应为置1,表示监听该socket,最大的socket+1就是set的有效长度,也是内核遍历次数。读、写、错误事件分别对应不同的set,select的第二、三四个函数既是传入参数又是返回值。
比一请求一线程好在哪里,难以突破c10k,select能够解决大部分,1024个fd,多做几个select,c10k可以突破,但是难以突破c1000k,为什么呢,select需要把rset集合copy到内核里边,可能集合里边就一个可读,然后再返回出来。对于大量的fd的数量select还是有局限的。
我们来分析一下poll
和select的监听机制是差不多的,但是在区分记录读、写、错误事件返回给用户的方式不同,select选择以3个独立的set区分,而poll是将其记录在返回的结构体的字段中。因此接口也有些不同。
struct pollfd fds[POLL_SIZE] = {0};//POLL_SIZE=1024
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);//
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;
close(i);
}
if (--nready == 0) break;
}
}
}
epoll有了一个epoll的表去存取所有的内容,就相当于一栋楼,有了个快递员利用一个信箱去处理整栋楼住户想处理的信息。
//poll/select -->
// epoll_create 创建一个集合,快递员负责的那栋楼
// epoll_ctl(ADD, DEL, MOD) 添加,删除,第三个就是修改
// epoll_wait 多长时间执行一次,快递员多久去看一次
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);//在5这个时间内快递员去处理盒子里边的内容
if (nready == -1) {
continue;
}
int i = 0;
for (i = 0;i < nready;i ++) {//处理请求
int clientfd = events[i].data.fd;
if (clientfd == listenfd) {//如果是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) {//如果不是listenfd,判断是否可读
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);//这里先recv再send的做法是比较业余的,以后会改正
} else if (n == 0) { //
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
close(clientfd);
}
}
}
}
epoll_wait(int epfd, epoll_event events, int max events, int timeout)
底层将注册和监听的逻辑分开,不是每次都要遍历fd进行添加和删除。epoll_wait的第二个参数,用户层有了rdlist,直接返回有事件的socket的引用,不再需要遍历判断是否有事件,直接拿出来用就可以,效率比较高。
服务器核心的点就是一个循环,反复去轮询检查是否有事件可以处理
关于epoll的知识,还有更多的东西还未讨论,未完待续。。。