前言
由于资源消耗大、线程管理复杂、性能题目等原因,“一哀求一线程”(多线程)已经被淘汰,现在我们多使用select、poll、epoll等IO多路复用技能实现网络IO。我们只讲解实现思路,而且为后面IO多路复用技能的文章打下基础。在看本文之前,可以先看一看 网络IO以及socket编程 这篇文章。
socket使用流程
在网络通信中,一定是使用socket。下面初始化socket的固定流程。
- #include <netinet/in.h>
- #include <stdio.h>
- #include <sys/socket.h>
- #include <errno.h>
- #include <string.h>
- int main()
- {
- int sockfd = socket(AF_INET, SOCK_STREAM, 0);
- struct sockaddr_in sevaddr;
- sevaddr.sin_family = AF_INET;
- sevaddr.sin_addr.s_addr = htonl(INADDR_ANY);
- sevaddr.sin_port = htons(2000);
- if (bind(sockfd, (struct sockaddr*)&sevaddr, sizeof(struct sockaddr)) == -1) {
- printf("bind failed: %s\n",strerror(errno));
- return -1;
- }
- listen(sockfd, 10);
- printf("listen finished\n");
- getchar(); // 阻塞在这里
- printf("exit\n");
- return 0;
- }
复制代码 实现思路
使用socket()创建一个sockfd,用于监听端口。第一个参数AF_INET表示使用IPV4协议族;第二个参数SOCK_STREAM创建流式套接字类型,即TCP;第三个参数0对于 SOCK_STREAM 类型的套接字,通常设置为0,表示使用默认的传输层协议。
- int sockfd = socket(AF_INET, SOCK_STREAM, 0);
复制代码 使用bind()将创建的sockfd与本机端口举行绑定。看下面的代码,我们要给本地端口指定通信协议族、初始化网卡地址、初始化端标语、绑定fd和本机端口。INADDR_ANY,即0.0.0.0,指本机任意网卡;在计算机中,0~1023是默认的端标语,所以我们要指定的端口后须大于1023,这里我们给定的端标语是2000;接下来,我们使用bind()绑定,若函数返回-1,则绑定失败。
- struct sockaddr_in sevaddr;
- sevaddr.sin_family = AF_INET; //IPV4
- sevaddr.sin_addr.s_addr = htonl(INADDR_ANY); //0.0.0.0
- sevaddr.sin_port = htons(2000); // 0-1023
- if (bind(sockfd, (struct sockaddr*)&sevaddr, sizeof(struct sockaddr)) == -1) {
- printf("bind failed: %s\n",strerror(errno));
- return -1;
- }
复制代码 完成上面的步调,我们就可以对端口举行监听了。
我们可以先对步伐编译运行,我们可以看到受getchar()的影响,步伐处于阻塞状态。
我们敲一下回车,步伐退出。
现在,我们再打开一个新的终端,输入下令netstat -anop | grep 2000查看端口2000的使用情况。我们可以看到端口2000已经被使用。
或者,我们可以在第二个终端中运行该步伐。编译器返回bind failed: Address already in use,很好,这表明端口已经被占用,这是我们想要看到的结果。
netassist
在这里,向大家安利netassist(网络调试助手),作为我们的客户端。这里我直接使用127.0.0.1(本地回环地址)作为我的远程主机地址。
现在,我们连接已经运行的步伐。可以看到,连接乐成了!
因为listen实行乐成而且客户端已经连接,现在我们通过netstat查看端口状态。可以看到,产生了一个新的连接状态。
既然已经连接上了,那么是否可以发数据?固然可以了!
点击“发送”,看到netassist的右下角的TX从0变为20,表示发送了20字节的数据到server端。可是,我们在server端并没有看到发送的数据,可以确定的是server已经接收到了信息。因为,server端临时不能将接收到的数据打印出来。
小总结
- 端口被绑定后,不能再次绑定。
- 实行listen,可以通过netstat查看IO状态。
- 进入listen可以被连接,而且产生新连接状态。
- io与tcp连接。P6中,每天生一条信息代表中一个IO。我们现在没有给它分配IO,我们须要使用accept。
第一版服务器
我们要解决几个已知的题目:
实现思路
- #include <errno.h>#include <netinet/in.h>#include <stdio.h>#include <string.h>#include <sys/socket.h>int main(){ int sockfd = socket(AF_INET, SOCK_STREAM, 0);
- struct sockaddr_in sevaddr; sevaddr.sin_family = AF_INET; sevaddr.sin_addr.s_addr = htonl(INADDR_ANY); sevaddr.sin_port = htons(2000); if (bind(sockfd, (struct sockaddr*)&sevaddr, sizeof(struct sockaddr)) == -1) { printf("bind failed: %s\n", strerror(errno)); return -1; } listen(sockfd, 10);
- printf("listen finished: %d\n",sockfd); struct sockaddr clientaddr; socklen_t len = sizeof(clientaddr); printf("accept:\n"); int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len); printf("accept finished\n"); char buffer[1024] = { 0 }; int count = recv(clientfd, buffer, 1024, 0); printf("RECV: %s\n", buffer); count = send(clientfd, buffer, 1024, 0); printf("SEND: %d\n", count); getchar(); printf("exit\n"); return 0;}
复制代码 我们使用accept函数等候客户端的连接,使用recv接收客户端发送的数据。
我们先编译运行这段代码。
现在可以看到,不管我们敲击多少次回车,步伐依旧处于阻塞的状态。那么步伐阻塞在那里呢?
真相就是——int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);,现在步伐等候着客户端的连接。
我们现在举行连接。可以看到,步伐已经跑出了accept的阻塞。现在收到来自recv的阻塞。
当步伐收到来自accept的阻塞时,我们可以看到tcp连接状态只有一个。
当我们连接server后(阻塞在recv处),tcp连接状态会出现两个。
这表明了,代码中实现的两个fd(sockfd和clientfd)与tcp连接信息是一对一的关系,百万级就有百万个。
出现的题目
当我们打开两个netassist(两个客户端)并连接时,是可以连接的而且产生了新的连接信息。可是,由于只有一对sockfd和clientfd,新的客户端并不能举行通信。
第二版服务器
既然要连接多个客户端,是否可以加进循环?可以。
- while(1){
- printf("accept:\n");
- int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
- printf("accept finished\n");
- char buffer[1024] = { 0 };
- int count = recv(clientfd, buffer, 1024, 0);
- printf("RECV: %s\n", buffer);
- count = send(clientfd, buffer, 1024, 0);
- printf("SEND: %d\n", count);
- }
复制代码 现在,我们运行后,打开三个客户端,依次(按照1,2,3的次序)举行连接而且发送。
可是这里有致命的题目!当我们依次(按照1,2,3的次序)连接不发送时,这时我们必须按照1,2,3的次序才气发送乐成。这样就酿成了只要前一个不接收到数据,后面的都接收不到。
原因很简朴,就是代码逻辑的题目。当我们依次点击连接后,第一个已经实行完accept函数,而后面两个阻塞在accept处且已经建立连接,但是并没有实行accept。第一个客户端阻塞在recv处,现在只有第一个发送数据并接收回发数据,后面的才气举行。这就扳连到了多路IO(多个客户端)怎么及时响应。
第三版服务器
使用线程
为了避免阻塞在recv处,我们为每一个客户端开一个线程。
- void* client_pthread(void* arg)
- {
- int clientfd = *(int*)arg;
- // 这里的循环是为了让同一个客户端收发多次数据
- while (1) {
- char buffer[1024] = { 0 };
- int count = recv(clientfd, buffer, 1024, 0);
- printf("RECV: %s\n", buffer);
- count = send(clientfd, buffer, count, 0);
- printf("SEND: %d\n", count);
- }
- }
- ......
- while (1) {
- printf("accept\n");
- int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
- printf("accept finished: %d\n", clientfd);
- pthread_t thid;
- pthread_create(&thid, NULL, client_pthread, &clientfd);
- }
- ......
复制代码 这样我们就可以解决recv阻塞的题目了。
解决题目
上面的代码已经可以完成多个客户端独立地发送接收数据,可是还不敷完善。当我们断开连接时,步伐会不停跑,而且CPU占用率极高。
出现这个征象的原因,是因为我们没有处理客户端断开的代码。现在我们仔细看上图,断开之后server返回的count值为0,证实当我们断开时,recv返回0。现在我们加入断开处理代码。
- void* client_pthread(void* arg)
- {
- int clientfd = *(int*)arg;
- // 这里的循环是为了让同一个客户端收发多次数据
- while (1) {
- char buffer[1024] = { 0 };
- int count = recv(clientfd, buffer, 1024, 0);
- printf("RECV: %s\n", buffer);
- if (count == 0) { // disconnect
- printf("client disconnect: %d\n", clientfd);
- close(clientfd);
- break;
- }
- count = send(clientfd, buffer, count, 0);
- printf("SEND: %d\n", count);
- }
- }
复制代码 运行一下:
可以看到已经可以正常地断开连接,而且返回对应的clientfd。
关于fd
从上图可以知道,fd的值是递增的。当步伐从入口函数main进入时,遇到的第一个fd(sockfd),给它赋值为3。哎,0、1、2在那里呢?来看下图,当我们实行ls /dev/fd时,可以看到0、1、2被占用,再依次实行ls /dev/stdin -l、ls /dev/stdout -l、ls /dev/stderr -l,可以看到0、1、2分别被标准输入、标准输出、标准错误输出占用;而3被网络连接占用。
那么,不停递增数量是否是有限的呢?答案是肯定的。运行ulimit -a看一下,可以看到open files的数量是有限的,所以,IO的数量也是有限的。IO也是fd。这里就可以明确Linux 一切皆文件(Linux操纵全部设备一切皆文件形貌符)了吧。操纵文件是文件,操纵socket是文件,操纵定时器也是文件。
现在再来观察一个征象,当我们断开clientfd后,又再次建立连接,体系会分配一个新的fd值:7,4被接纳了;我们现在又断开,等一段时间又连接,可以看到体系又分配了4。那又须要等候多长时间呢?等候TIME_WAIT,而这个时间是可以设置的。
总结
至此,“一哀求一线程”方式的server已经实现完成。在开篇我们提到这种形式的server已经被淘汰,它有优点也有缺点。
优点:实现简朴。
缺点:不利于并发。线程的并发量就等于客户端的数量,大大占用了资源。
这样我们就引入了IO多路复用。(后面会写)
附件
- #include <errno.h>#include <netinet/in.h>#include <pthread.h>#include <stdio.h>#include <string.h>#include <sys/socket.h>#include <unistd.h>void* client_pthread(void* arg)
- {
- int clientfd = *(int*)arg;
- // 这里的循环是为了让同一个客户端收发多次数据
- while (1) {
- char buffer[1024] = { 0 };
- int count = recv(clientfd, buffer, 1024, 0);
- printf("RECV: %s\n", buffer);
- if (count == 0) { // disconnect
- printf("client disconnect: %d\n", clientfd);
- close(clientfd);
- break;
- }
- count = send(clientfd, buffer, count, 0);
- printf("SEND: %d\n", count);
- }
- }
- int main(){ int sockfd = socket(AF_INET, SOCK_STREAM, 0);
- struct sockaddr_in servaddr; servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(2000); if (bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr)) == -1) { printf("bind failed: %s\n", strerror(errno)); return -1; } listen(sockfd, 10);
- printf("listen finished: %d\n", sockfd); struct sockaddr_in clientaddr; socklen_t len = sizeof(clientaddr);//第一版#if 0 printf("accept\n"); int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len); printf("accept finished\n"); char buffer[1024] = { 0 }; int count = recv(clientfd, buffer, 1024, 0); printf("RECV: %s\n", buffer); send(clientfd, buffer, count, 0); printf("SEND: %d\n", count);//第二版#elif 0 while (1) { printf("accept\n"); int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len); printf("accept finished\n"); char buffer[1024] = { 0 }; int count = recv(clientfd, buffer, 1024, 0); printf("RECV: %s\n", buffer); send(clientfd, buffer, count, 0); printf("SEND: %d\n", count); }//第三版#else while (1) { printf("accept\n"); int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len); printf("accept finished: %d\n", clientfd); pthread_t thid; pthread_create(&thid, NULL, client_pthread, &clientfd); }#endif getchar(); printf("exit\n"); return 0;}
复制代码 (欢迎勘误)
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |