网络编程1:socket编程基础入门
本章核心:
网络基础知识入门
如何通过编程实现网络通信
什么是服务器-客户端架构以及如何用2实现
***********************************我是分割线***********************************
网络的整体架构和行为直观且易于理解:通信设备被相互连接以交换信息,计算机通过连接这些设备中的某一台来接入由这些通信设备共同构成的“网络”。如下图中蓝色设备所示。
有了这样一个网络,发送数据从而实现通信就很简单:计算机将数据发送给与其直接相连的通信设备,接下来整个网络遵循某种算法,通过上述结构将信息从该设备送到与目标计算机连接的通信设备,这样便实现了两台计算机之间的通信。尽管这其间有许多细节亟待解释,例如连接通信设备的网络结构具体是怎样的?这个结构又遵循什么算法?两台设备本身的通信又如何实现?等等,但请暂时将它当作一个黑盒。让我们先作为网络的用户,网络接口的调用者学习如何使用它。
虽说如此,为了方便我们还是给设备们都“起个名字”。上图中间被涂成蓝色的部分,即为前文提到的相互连接用于交换信息的通信设备,我们称之为核心网;而核心网周围连接它们从而接入网络的设备则称为边缘网。它们都是计算机设备。下面我们直入主题,学习如何在我们每天都会使用的手机,PC等边缘设备中编程,从而通过网络交换信息。
1.1 第一个Socket程序
计算机上唯一用于接入网络的硬件设备网卡由内核中的驱动程序控制,让用户态程序经过操作系统间接发送数据到网卡的接口socket()也由操作系统提供。socket()是类UNIX环境下提供给用户态程序的网络接口,几乎所有的网络通信,都是通过调用其他网络通信函数操作它展开。
如何使用socket()?网络相关的接口,网络的原理可以很复杂,但使用网络无非发送和接收数据而已,和现实世界中收发信件一模一样。现实世界中寄出一份纸质信件无非注明收发信息,选择普件/急件/是否需要保险等邮寄方式,将信放入邮筒。在网络世界中使用socket()也类似,以下代码按照上述流程实现了发送方的功能,给目标发送了一句“Hello!”:
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h> // sockaddr_in, inet_pton
#include <sys/socket.h> // socket(), sendto()
#include <unistd.h> // close()
int main() {
// 创建 IPv4 UDP socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 设置目标地址
struct sockaddr_in dest_addr;
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.sin_port = htons(2000);
dest_addr.sin_family = AF_INET;
const char* target_ip = ""; //可以通过ifconfig命令查看本地ip地址并相应修改
inet_pton(AF_INET, target_ip, &dest_addr.sin_addr);
// 发送消息
const char* message = "Hello!";
ssize_t sent_bytes = sendto(sockfd, message, strlen(message), 0,
(struct sockaddr*)&dest_addr, sizeof(dest_addr));
close(sockfd);
return 0;
}
下图左边展示了刚刚编写的数据发送方代码的流程,右边则是接下来会实现的接收方。
先关注左边:
程序首先调用socket()得到了一个int类型的返回值sockfd,是套接字文件描述符socket file description的缩写,其底层是类UNIX环境下的I/O文件描述符。可是我们不是在讲网络吗?和I/O或者文件有什么关系?由于对于文件,I/O,网络等等资源的操作都可以概括为“读”和“写”,UNIX为这些资源都提供了一套行为一致的系统调用和管理方式。在这里暂且先不深究其底层实现,在用户视角的我们只需要知道,它们都被视为文件,管理它们的标识符则都被称为文件描述符fd,也就是调用socket()返回的int类型值。我们可以直接通过fd把它们当作文件一样操作。接下来一步是告诉操作系统“收件人与地址”。网络虽不像现实世界使用“人可以读懂的具体位置”来标注地址,但仍然是类似“一个地址指明一栋建筑”的结构。你或许此前已经听过,网络中这个指明数据接收方计算机的地址被称为ip地址。此外,由于该计算机上可能同时存在多个socket,向其中哪一个发送数据也需要指定,这个指定号码被称作端口。C语言要求把这些信息集中于包含在头文件“arpa/inet.h”的结构体sockaddr_in中。那么上面设置地址的流程就很明确了:1. 声明结构体dest_addr并通过memset()初始化。2. 设置端口。调用htosn()将端口号2000标准化(在C中必须通过这个函数设置端口),3. 设置ip地址。设置地址类型AF_INET,也即ipv4地址——为我们上面提到的ip地址第四版,并调用inet_pton()将参数target_ip转化为二进制形式赋值给&dest_addr.sin_addr(同样必须通过这个函数设置ip地址)。
至此,端口和ip地址都已经被包含在了结构体dest_addr中,调用sendto()发送信息即可。五个参数中的第四个参数0代表“按照默认行为发送”,另外四个看参数名都很好理解就不赘述了。
最后在函数返回前记得调用close(sockfd)回收资源。
尽管因为有很多网络的理论知识目前还未涉及导致上述代码中很多细节只能模糊带过,但发送的整体流程很清晰。对应的接收流程类似,如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h> // sockaddr_in, inet_ntop
#include <sys/socket.h> // socket(), bind(), recvfrom()
int main() {
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 准备本地地址(接收数据的地址)
struct sockaddr_in recv_addr;
memset(&recv_addr, 0, sizeof(recv_addr));
recv_addr.sin_port = htons(2000);
recv_addr.sin_family = AF_INET;
recv_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有本地IP
bind(sockfd, (struct sockaddr*)&recv_addr, sizeof(recv_addr));
// 接收消息
struct sockaddr_in send_addr;
socklen_t send_len = sizeof(send_addr);
char buffer[1024];
ssize_t recv_len = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr*)&send_addr, &send_len);
buffer[recv_len] = '\0';
printf("Received message: \"%s\"\n", buffer);
close(sockfd);
return 0;
}
和发送方相比主要有两点区别:
需要额外调用bind()来将申请到的socket和某个端口绑定,这对应了发送方向接收方的指定端口发送数据。
需要开辟缓冲区,调用recvfrom()接收数据。
也即,发送方仅需sendto()一步发送数据,但接收方需要bind()+recvfrom()两步接收。
至此我们已经成功通过socket实现了一次信息发送!你可以跟着上述流程尝试自己实现,编译,运行。注意,在尝试运行时需要先./recv,再./send。网络数据传递的延迟仅以ms为单位。如果先./send,在还没来得及./recv的时候数据就会送达,并因为没有socket接收而被丢弃。反之,先运行/recv程序会阻塞在recvfrom()直接收到数据为止。如果你想尝试编写自己的程序,未说明的参数直接照抄即可,或者继续往下看。
前面说过,网络通信从编程的角度来说就是调用各种网络相关的函数操作sockfd。操作sockfd的函数肯定远不止上面这些,使用sockfd的方法也不止这一种。下面将介绍一种与上述方法不同的操作socket的方式,并引出相关的函数。尽管目前暂时还没有系统介绍网络底层如何传输数据,但有一个事实可以提前给出:网络的底层数据传输不可靠。网络底层架构的固有缺陷导致发出的数据在传输过程中可能会被打乱顺序,部分损坏乃至丢失。尽管概率不高,但上一节实现的代码所发出的数据的确会面临这种风险!因此,一种可靠的数据传输方法在软件层面,而非在架构层面诞生。尽管它仍然基于网络底层的不可靠传输,但可以保证传输的数据是可靠的。仍旧先忽略原理介绍用法,通过socket使用它仍然不复杂。代码如下:
接收方:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main() {
int recv_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in recv_addr;
memset(&recv_addr, 0, sizeof(server_addr));
recv_addr.sin_port = htons(2000); // 监听端口
recv_addr.sin_family = AF_INET;
recv_addr.sin_addr.s_addr = INADDR_ANY; // 所有本地IP
bind(recv_fd, (struct sockaddr*)&recv_addr, sizeof(recv_addr));
listen(recv_fd, 1);
struct sockaddr_in send_addr;
socklen_t send_len = sizeof(send_addr);
int conn_fd = accept(recv_fd, (struct sockaddr*)&send_addr, &send_len);
char buffer[1024];
ssize_t conn_len = recv(conn_fd, buffer, sizeof(buffer)-1, 0);
buffer[conn_len] = '\0';
printf("Received: %s\n", buffer);
close(conn_fd);
close(recv_fd);
return 0;
}
发送方:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main() {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in recv_addr;
memset(&recv_addr, 0, sizeof(recv_addr));
recv_addr.sin_port = htons(2000);
recv_addr.sin_family = AF_INET;
const char* recv_ip = ""; //可以通过ifconfig命令查看本地ip地址并相应修改
inet_pton(AF_INET, recv_ip, &recv_addr.sin_addr);
connect(sockfd, (struct sockaddr*)&recv_addr, sizeof(recv_addr));
const char* message = "Hello!";
send(sockfd, message, strlen(message), 0);
close(sockfd);
return 0;
}
流程图:
新实现和上一节实现的主要区别在接收方:
申请socket时需要传递的第二个参数由SOCK_DGRAM变成了SOCK_STREAM。这个内容在上一节被刻意忽略了。实上,它们分别代表了以UDP和以TCP的方式传输数据。前者是上一节采用的方式,不可靠;后者是这一节采用的方式,可靠。
接收方在bind()和recv()函数之间多了两个额外步骤,listen(recv_fd , 1)和accept()。这两部都是在建立连接。为了实现可靠的数据传输,TCP需要首先在收发双方之间建立连接,而之前实现的UDP无需在收发数据前建立连接。listen()告诉操作系统:开始关注,或者用术语叫做开始监听,有没有其他socket通过网络向recv_fd发起的连接申请。如果有多个连接申请同时发起,最多保留n个,在这里是1个。accept()告诉操作系统:阻塞等待直到有连接申请,和新的连接申请建立连接并返回一个新的文件描述符conn_fd。
UDP和TCP在接收流程上的区别有点像:UDP接收方直接就是“电话接听者”,调用recvfrom()返回的直接就是数据;而TCP传输则先有一个接线员recv_fd,通过调用accept()得到的conn_fd才是“电话接听者”。
TCP和UDP发送方的实现则变化不大,只是因为需要发起连接而多调用了一个connect()。此函数会阻塞等待直到接收方成功建立连接。
目前我们为止介绍了UDP和TCP两种不同的申请socket()的方式,操作这两种socket()的函数也不一样。读到这里你肯定还感受不到它们两者的区别,也不知道应该在什么情况下使用两者中的哪一种。这很正常,下一节的末尾会有一个小练习,完成它会对熟悉这些接口很有帮助。
1.2 客户端——服务器架构
听起来或许很不可思议,但是仅需上一节介绍的这么点内容,就足够创建一个服务器供他人访问了。为方便接下来的讨论,我们同样得首先给这个场景中的各个元素们“各起一个名字”。
绝大多数网页,乃至各种app背后都是客户端——服务器架构。客户端会基于用户的操作,例如点击,输入内容等生成数据,并通过网络发送给服务器,称之为请求;服务器解析数据后组织好对应的数据,再通过网络返回给对应的客户端,称之为响应。访问网址其实就这么回事。以网页访问为例,对客户端也即浏览器来说,网址其实是一个ip地址,输入网址即向该ip地址的机器发出请求建立TCP连接以获得数据,服务器收到请求后响应,建立连接并传输数据,最后浏览器收到数据并渲染成网页。在这种架构下,服务器24/7不间断运行,通过网络处理来自多个客户端的多次请求。
观察上一节的代码,能发现“发送方”的代码已经几乎是一个客户端的请求发送模块了,但“接收方”还不能作为一个服务器。其中最大的问题是,“接收方”只能接收一次请求,之后便会停止运行,无法支持客户端多次访问。当然这也并不难。要实现多次访问,很自然会想到修改listen()的参数扩容监听队列,并循环处理连接请求来实现。我们先不管网页的事,实现一个可以反复建立连接,收发数据的服务器。代码如下:
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h> // for close()
#include <arpa/inet.h> // for inet_ntop()
#define BUF_SIZE 1024
int main() {
int recvfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in recv_addr;
memset(&recv_addr, 0, sizeof(recv_addr));
recv_addr.sin_family = AF_INET;
recv_addr.sin_port = htons(2000);
recv_addr.sin_addr.s_addr = INADDR_ANY; // 所有本地IP
bind(recvfd, (struct sockaddr*)&recv_addr, sizeof(recv_addr));
listen(recvfd, 10);
while (1) {
struct sockaddr_in client_addr;
socklen_t clientlen = sizeof(client_addr);
int connfd = accept(recvfd, (struct sockaddr*)&client_addr, &clientlen);
char buffer[BUF_SIZE];
ssize_t n = recv(connfd, buffer, BUF_SIZE - 1, 0);
if (n > 0) {
buffer[n] = '\0';
printf("Received from client: %s\n", buffer);
const char *reply = "Message received\n";
send(connfd, reply, strlen(reply), 0);
} else {
printf("recv failed or client closed connection\n");
// 错误处理
}
close(connfd);
printf("Connection closed\n");
}
}
改动并不复杂,就不做详细说明了。在本地开启两个终端,一个运行服务器一个多次运行上一节的tcp_send即可查看效果。当然这份代码是有问题的,没有结束条件只能Ctrl+C退出,也因此没办法回收recvfd。所以在完成下面这个小练习时请注意一下这一点:
服务器:执行时接收一个数字参数,记为密码pwd。
协商阶段。启动后,将一个udp socket随机绑定到端口号p1,称之为协商socket,并将端口p1输出,等待接收客户端数据。如果接收到格式严格为”d% d%”(数字+空格+数字)的字符串,将第一个数字解析为密码与pwd对比,相同则进入连接阶段。否则向客户端发送字符”0“(注意不是空字符”\0“)。
连接阶段。将第二个数字解析为端口号p2,监听来自相同客户端上p2端口的TCP连接。连接成功建立后,接收客户端发送来的文件,统计字节数并回发给客户端。至此连接结束,回到协商阶段等待下一次协商。
客户端:执行时接收两个数字参数,第一个记为密码pwd,第二个为服务器启动时打印的端口号p1(这是在模拟已知服务器端口)。
协商阶段。使用udp socket向服务器上p1端口发送格式严格为”d% d%”(数字+空格+数字)的字符串,意义同上,等待服务器回发。如果回发为”0“则意味密码错误,程序结束,如果大于0则记为端口号p2,进入连接阶段。
连接阶段。向端口号p2发起tcp连接,连接成功并发送文件后等待服务器统计文件长度并回发数据。接收到结果后将字节数输出,程序结束。
实现上述客户端服务器所需要的socket编程知识基本上都已经介绍了,但是还需要对很多情况进行错误处理,以保证服务器即使遇到错误,也能成功恢复并回到协商阶段等待下一次协商,这是服务器24/7运行的基础。
1.3 高级网络IO
在上一节结尾我们提到,一般情况下并发才是服务器要面对的主要、核心问题。在上一章的实现中,就算存在活跃的连接,一旦程序执行到accept()且没有新的连接请求,那程序就会一直阻塞在accept()而无法执行其它活跃连接的读写。为实现并发,不难朴素地想到可以通过“一请求一线程”的方式,即每建立一个新的连接就为其开一个新的线程来实现并发。这种架构直观且思维难度小,但在工程上是不实际的,甚至都无需用代码来展示。工业级服务器面对的并发数量通常是数万,数十万,甚至上百万。采用上述方法意味着线程数量会随连接数量线性增加到百万级,带来昂贵的上下文切换开销和巨大的内存压力。
回想一下一般的网络场景。单个socket不间断持续收发数据是少数情况,多数时间绝大多数建立连接的sockfd都在阻塞等待数据。那么,我们能否将阻塞等待单个socket的模式更改为,在每一轮循环中忽略还在等待数据的socket,用某种方法找出所有能够接收数据的socket并处理呢?更进一步,除了找出能接收数据的sockfd,我们可不可以同样只找出能写入数据的sockfd和有异常发生的sockfd呢?换言之,有没有一种方法可以帮助用户态程序统一管理fd,并监控与这些fd相关的“有数据可以被读取”的“读事件”,“准备好发出数据”的“写事件”和“socket出错需要处理异常”的“异常事件”呢?I/O多路复用就提供了这样的“魔法“。用户态程序将关注的fd交给操作系统代为管理,仅需处理由操作系统返回的事件就绪的fd,从而用单线程程序实现多个网络连接的管理。本章会将介绍select和poll。它们的作用类似,但管理集合的方式不同,也因此会调用不同的接口,传递不同的参数。下面先讲select():
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h> // for close() #define BUF_SIZE 1024
int main(){ int recvfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in recv_addr;
memset(&recv_addr, 0, sizeof(recv_addr));
recv_addr.sin_family = AF_INET;
recv_addr.sin_port = htons(2000);
recv_addr.sin_addr.s_addr = INADDR_ANY;
bind(recvfd, (struct sockaddr*)&recv_addr, sizeof(recv_addr));
listen(recvfd, 10);
struct sockaddr_in client_addr;
socklen_t clientlen = sizeof(client_addr);
fd_set rfds, readfds;
FD_ZERO(&rfds);
FD_SET(recvfd, &rfds);
int maxfd = recvfd + 1;
while(1){
readfds= rfds;
select(maxfd, &readfds, NULL, NULL, NULL);
if (FD_ISSET(recvfd, &readfds)){
int clientfd = accept(recvfd, (struct sockaddr*)&client_addr, &clientlen);
FD_SET(clientfd, &rfds);
if (clientfd >= maxfd) maxfd = clientfd + 1;
}
for (int i = 0; i < maxfd; ++i){
if (i == recvfd) continue;
if (FD_ISSET(i, &readfds)){
char buffer[BUF_SIZE] = {0};
ssize_t n = recv(i, buffer, BUF_SIZE-1, 0);
if (n > 0){
buffer[n] = '\0';
printf("Received from client: %s\n", buffer);
const char *reply = "Message received\n";
send(i, reply, strlen(reply), 0);
} else if (n == 0) {
close(i);
FD_CLR(i, &rfds);
continue;
}
}
}
}
}
select()调用本身总共要求五个参数:
第二、第三、第四为如下图所示三个集合的指针,分别为readfds、writefds和exceptfds,分别代表读事件集合、写事件集合和异常事件集合,是交互的核心。这三个集合都是定长的位数组,下标即代表对应值的fd。也即,第0位代表值为0的fd,第1位代表值为1的fd,以此类推。
调用select()以前,用户态程序将需要监控的fd所对应的位修改为1,代表关心该fd的事件。操作系统监控到事件就绪以后会对应修改这三个集合,如果某个fd有对应事件就绪则不会修改,对应位仍为1;如果没有事件就绪则修改为0。例如,用户态程序将readfds的第3位、5位和6位(从0开始)修改为1后调用select(),操作系统则监控值为3、5、6的三个fd是否有事件就绪。恰好值为6的fd有写事件就绪而3和5没有,则将第3和第5位修改为0后返回。换言之,用户态和内核通过修改三个集合的值来交互。集合在调用select()前某位为1代表“监控对应fd”,select()返回后某位为1代表“对应fd有事件就绪”。虽然三个fd_set都为定长位数组,但select()并不会把三个fd_set都从头到尾检查一遍。其第一个参数maxfd代表最大检查下标,大于maxfd的位忽略。
第五个参数控制select()的timeout行为。上面示例代码中的NULL代表阻塞等待直到有任意fd被事件触发,如果全体集合都没有被触发则一直阻塞等待。当然,也可以不阻塞直接返回,如果调用时没有任何事件就绪则全改为0;或者设置超时时间,如果超时还未有任何事件就绪则全改为0,如果超时前有任意事件就绪则修改后立刻返回。
了解了select()的参数,剩下新引入用于操作fd_set的函数就不难理解了:
FD_ZERO(&rfds),清空整个fd 集合,将rfds中所有的位设0,为初始化函数。
FD_SET(recvfd, &rfds),关注某个fd,将recvfd对应位设为1。
FD_ISSET(recvfd, &rfds),查询fd是否在集合中,返回true或false。
FD_CLR(recvfd, &rfds),不再关注某个fd,将recvfd对应位设为0。
最后是上面代码的逻辑。除了因关心读事件而申请了一个readfds以外,还申请了一个不分事件种类的全集rfds。进入循环体,调用select(),再调用FD_ISSET()检查recvfd的读事件,如有新请求则建立连接并将fd加入全集rfds;之后进入for循环逐个检查读事件,将recv失败,也即连接断开的fd调用FD_CLR()清除并回收资源;最后在下一轮循环开始时用rfds更新readfds。
从使用体验的角度来说,select()需要的参数有点太多了。此外无论关注多少个fd,select()都需要从0到maxfd扫描整个位数组,并且还要扫描三次。poll()则的行为虽然在用户态程序的视角下和select()的行为基本一致,但是因部分改进了上述问题从而大大简化了调用所需的参数,同时提高了效率。现如今除了比较老的版本,基本都不使用select()而改为使用poll()了。
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h> // for close() #include <poll.h>
#define BUF_SIZE 1024
int main(){
int recvfd = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in recv_addr;
memset(&recv_addr, 0, sizeof(recv_addr));
recv_addr.sin_family = AF_INET;
recv_addr.sin_port = htons(2000);
recv_addr.sin_addr.s_addr = INADDR_ANY;
bind(recvfd, (struct sockaddr*)&recv_addr, sizeof(recv_addr));
listen(recvfd, 10);
struct sockaddr_in client_addr;
socklen_t clientlen = sizeof(client_addr);
struct pollfd fds[BUF_SIZE] = {0};
fds[recvfd].fd = recvfd;
fds[recvfd].events = POLLIN;
int maxfd = recvfd + 1;
while(1){
poll(fds, maxfd, -1);
if (fds[recvfd].revents & POLLIN){
int clientfd = accept(recvfd, (struct sockaddr*)&client_addr, &clientlen);
fds[clientfd].fd = clientfd;
fds[clientfd].events = POLLIN;
if (clientfd >= maxfd) maxfd = clientfd + 1;
}
for (int i = 0; i < maxfd; ++i){
if (i == recvfd) continue;
if (fds[i].revents & POLLIN){
char buffer[BUF_SIZE] = {0};
ssize_t n = recv(i, buffer, BUF_SIZE-1, 0);
if (n > 0){
buffer[n] = '\0';
printf("Received from client: %s\n", buffer);
const char *reply = "Message received\n";
send(i, reply, strlen(reply), 0);
} else if (n == 0) {
close(i);
fds[i].fd = -1;
fds[i].events = 0;
continue;
}
}
}
}
}
如果已经理顺了select()的行为,你会发现poll()基本没啥要讲的。它和select()唯一的不同就是管理fd的方式不一样。select()要求将fd装入三个关注不同的事件的不同集合,而poll()直接将每个fd+关注事件抽象为了一个结构体,并将所有的fd结构体用一个数组管理,每次调用poll()仅需传递一个数组。要求为每个fd都定义一个结构体pollfd,三个filed分别代表fd,关注什么事件,实际返回了什么事件:
struct pollfd {
int fd;
short events;
short revents;
};
此外还能注意到,events和revents都通过宏定义使用:
// .......
if (fds[recvfd].revents & POLLIN){
// ......
}
这里使用了位运算&。events,revents,POLLIN等实际上都是二进制数,被定义为0,1,2,4,8这些转换成二进制后只有某个bit为1其余都为0的值。这样直接&运算并判断结果是否为全0要比==运算快得多。
1.4 总结
本章我们从socket编程开始,一步一步搭建起了一个基础的服务器,实现了:
UDP/TCP的收发双方
基于TCP的循环服务器
select/poll
select()/poll()本身仍有一些可以优化的地方。例如,每次调用时操作系统都会把fd集合从用户态拷贝到内核态遍历,这种行为在大并发场景下也仍谈不上是效率最高的做法。有没有什么方法可以在利用内核的事件通知机制的同时,避免频繁的fd全集传参和遍历呢?下一章,我们将介绍使Linux成为服务端开发绝对主流的另一种高级I/O,并基于它引出一种全新的编程范式。