推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习: https://github.com/0voice 链接。
NetAssist 的下载和使用
我是零声教育的学员,主要学习的是 C/C++ 后端开发。对服务器的开发是一个重头戏,而测试服务器的工具就是 NetAssist(网络测试调试工具),
它可以模拟一个客户端,并给我们所设计的服务器进程发送信息,用以检测服务器的工作情况。
软件 NetAssist 的下载和使用可以参考这篇 文章。 软件 NetAssist 的界面如下
建立连接可以参考如下步骤
而后绑定本机地址
注意到本例中的本机 IP 192.168.152.1
是网络的“出入口管理者”,而远程 IP 192.168.152.128
是普通设备,二者是 客户端-网关
的关系。在调试工具中,发往 .1
的数据可能被路由到外网,而发往 .128
的数据仅在局域网内流转。192.168.152.128
和 192.168.152.1
是处于同一网段的两个不同IP地址,它们属于同一个网络,两个IP地址都在 192.168.152.0/24
网段内(子网掩码通常为 255.255.255.0),属于同一个局域网。
这是一个很经典的网络模型,访客把信息发给网关,网关再把信息发给服务器,服务器把报文返回给网关,网关再渲染给访客。
业务拆解
在本篇文章中,我们将介绍两个案例,两个都是最简单的服务器案例。服务器是一个进程的概念,也就是说它们是一个程序,而且该程序是负责网络 I/O 的,即输入输出。网络 I/O 分别由 recv
和 send
具体实现,都是来自头文件 <sys/socket.h>。
之所以称之为 “丐版” 是因为,这两个案例只是简单地调用了最核心的 I/O 函数,即 recv
和 send
,没有延时处理机制故而都是不能多路复用的。 recv
和 send
两个函数都需要特殊处理,才能实现多路复用,具体可见我的下一篇文章。
案例一
剥去现实服务器的层层外衣,只留下负责网络 I/O 的 recv
和 send
函数。该案例展示的是一个只有一次输入-输出事务的服务器。只能接受一次网络 I/O,服务器就关闭。
具体的流程图如下,
案例一的 C 代码
准备头文件
#include <errno.h> // 这是全局变量 errno,用于健壮的读取功能
#include <stdio.h>
#include <sys/socket.h> // 创建和管理套接字。绑定地址、监听连接和接受连接。发送和接收数据。设置和获取套接字选项。 socket()、connect()、sendto()、recvfrom()、accept()
#include <netinet/in.h> // 提供了结构体 sockaddr_in
#include <string.h>
#include <unistd.h> // close 函数,关闭套接字
为本服务器程序创建套接字,这个套接字是用来监听来访 IP 的
int init_server(unsigned short port) {
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); // 0.0.0.0
servaddr.sin_port = htons(port); // 0-1023,
// 设置端口复用
// 当服务器主动关闭 TCP 连接时,会进入 TIME_WAIT 状态(通常持续 2MSL,约 1-4 分钟)。在此期间,操作系统会保留该端口绑定记录,防止延迟到达的数据包干扰新连接。
// 问题:服务器崩溃或重启后尝试重新绑定端口时,会因 TIME_WAIT 状态导致 bind() 失败(错误:Address already in use)
// 以下处理措施:能避免再次启用服务器程序时,系统的宕机
int reuse = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
// 如果未设置 SO_REUSEADDR,导致端口被占用后无法立即重用(TIME_WAIT 状态)
perror("setsockopt failed: %s\n", strerror(errno));
return -1;
}
// setsockopt 是一个用于设置套接字选项的系统调用函数。
// 第二项参数 level:指定选项所在的协议级别。常见的值包括:SOL_SOCKET:表示套接字级别的选项。IPPROTO_TCP:表示 TCP 协议级别的选项。IPPROTO_IP:表示 IP 协议级别的选项。IPPROTO_IPV6:表示 IPv6 协议级别的选项。
// 第三项参数 optname:指定要设置的选项名称。不同的协议级别有不同的选项名称。例如:在 SOL_SOCKET 级别,常见的选项包括 SO_REUSEADDR、SO_KEEPALIVE、SO_LINGER 等。在 IPPROTO_TCP 级别,常见的选项包括 TCP_NODELAY 等。
// 第四项参数 optval:指向包含选项值的内存区域。选项值的类型和大小取决于 optname
// 第五项参数 optlen:指定 optval 的长度(以字节为单位)。
if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {
printf("bind failed: %s\n", strerror(errno)); // strerror 函数定义在 <string.h> 中,而 errno 是 <errno.h> 的全局变量
return -1;
}
// 一次监听 10 个来访 IP:PORT
// printf("listen finshed: %d\n", sockfd); // 3
if (listen(sockfd, 10) < 0) {
// 将套接字设置为被动模式:套接字从主动连接模式(用于客户端)转换为被动监听模式(用于服务器)。我们可以把这个 socket 想象成公司的前台小姐。
// 5:是监听队列的最大长度,表示系统可以为该套接字排队的最大未完成连接数。当新的连接请求到达时,如果队列已满,新的连接请求将被拒绝。
// 返回值:成功,返回 0。失败,返回 -1,并设置 errno 以指示错误原因。
printf("listen finshed: %d\n", sockfd);
return -1;
}
return sockfd;
}
程序中的结构体 sockaddr_in
来自头文件 <netinet/in.h>
。用于监听的 sockfd
套接字会把监听到的来访 IP 在系统内部分配一个文件描述符 clientfd
统一处理网络 I/O 事务。该服务器的代码如下。
#define PORT 2000
int main() {
int sockfd = init_server(PORT);
struct sockaddr_in clientaddr; // 申请地址的内存空间
socklen_t len = sizeof(clientaddr);
// 这是一个丐版的网络 I/O,只能接受一个 I/O
printf("accept\n");
int clientfd = accept(sockfd, (struct sockaddr *)&clientaddr, &len);
// accept 会因为无输入而阻塞
// 把监听到的 IP 地址记录下来,让 client_addr 去存储这个 IP,并且专门为这个 IP 创建套接字,以处理相关事宜
// accept 函数的作用是从监听套接字的连接队列中取出一个已完成的连接请求,并为该连接创建一个新的套接字描述符。这个新的套接字描述符专门用于与该客户端进行通信,而原来的监听套接字 sockfd 仍然继续监听新的连接请求。
// 返回值:成功:返回一个新的套接字描述符,用于与连接的客户端进行通信。失败:返回 -1,并设置 errno 以指示错误原因。
// 务必要注意的一点是,从始至终内存中只有一个 clientfd,客户端 IP 被其捕获之后,分配一个套接字与其远程连接,并被注册进了 EPOLL 之中
if (clientfd < 0) {
perror("accept failed\n");
} else {
printf("accept finshed\n");
}
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);
printf("exit\n");
return 0;
}
我们的服务器是建立在端口 2000
之上。要注意到 0~1023
端口是系统的端口,其余的端口都是可供用户选择的。
案例一的代码运行效果
代码编译,该程序只能接受一次网络 I/O,服务器就关闭。
qiming@qiming:~/share/CTASK/TCP_test$ gcc -o networkio networkio.c
执行程序,一开始并没有来访 IP,故而程序挂起。
qiming@qiming:~/share/CTASK/TCP_test$ ./networkio
accept
连接该服务器程序,该程序进程是建立在 IP 192.168.152.128
的端口 2000
之上,故而我们要在 NetAssist 上如下操作
建立连接后,Linux 的命令行的现状如下
qiming@qiming:~/share/CTASK/TCP_test$ ./networkio
accept
accept finshed
发送信息
发送信息后,Linux 命令行的变化
qiming@qiming:~/share/CTASK/TCP_test$ ./networkio
accept
accept finshed
RECV: Welcome to NetAssist
SEND: 20
exit
qiming@qiming:~/share/CTASK/TCP_test$
我们发现这个极简案例,只能接受一次网络 I/O,服务器就关闭。如此第一个案例便结束了。
案例二
第二个案例要稍微比第一个高级一些,因为它能实现多个来访 IP 的连接,并且每个连接都能执行一次网络 I/O,但也就只能发送一次了,因为本案例的代码中的 listen()
函数是无法监听到已经建立网络连接的来访 IP,而 accept
函数无法接受 listen()
所无法监听到的网络 IP,也就无法建立网络套接字 clientfd
。
案例二的 C 代码
该代码中的 init_server
函数还是跟案例一的一样,都是用于建立一个监听来访 IP 的文件描述符(套接字)sockfd
。
int main() {
int sockfd = init_server(PORT);
struct sockaddr_in clientaddr; // 申请地址的内存空间
socklen_t len = sizeof(clientaddr);
// 这也是一个丐版的网络 I/O,能接收多个 I/O
// 之所以还是丐版:连接一次后,互发消息一次后,就把别人撂在那里;listen() 不再处理已建立的连接(clientfd)
// 也就是没有二次回复
while (1) {
printf("accept\n");
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len); // listen() 不再处理已建立的连接(clientfd)
// listen() 不再处理已建立的连接(clientfd),故而 accept 函数不会接收到 listen() 所不愿监听的来访 IP
if (clientfd < 0) {
printf("accept failed\n");
} else {
printf("accept finshed\n");
}
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
无限循环,理论上可以建立无限个连接,但正如本案例中对 listen
和 accept
函数的解读,实际上每个连接只能完成一次网络 I/O 的任务。
案例二的代码执行情况
编译代码
qiming@qiming:~/share/CTASK/TCP_test$ gcc -o networkio networkio.c
执行代码,一开始并没有来访 IP 的时候,进程挂起。
qiming@qiming:~/share/CTASK/TCP_test$ ./networkio
accept
建立网络连接,并且发送信息,
但我们会发现,图中这个 发送 按钮只能按一次,按再多都是没有用的,命令行的代码如下,进程还会继续挂起,不会发更多信息。
qiming@qiming:~/share/CTASK/TCP_test$ ./networkio
accept
accept finshed
RECV: Welcome to NetAssist
SEND: 20
accept
但如果我们在 NetAssist 软件上开启新的一个会话,那么我就可以再发一条信息,
之后,我们在 Linux 命令行上就能看到我们又多发了一条消息。
qiming@qiming:~/share/CTASK/TCP_test$ ./networkio
accept
accept finshed
RECV: Welcome to NetAssist
SEND: 20
accept
accept finshed
RECV: Welcome to NetAssist
SEND: 20
accept
该进程还是会继续挂起的,我们需要再开一个会话才能再发一条消息。也就是说这些连接都是一次性的,用完即闲置。
读者可以在 Linux 命令行中按下 Ctrl + C 终止该进程。
总结
本篇文章主要介绍了两个极简,简到可称“丐版”,的服务器代码,这两个代码都是无法实现多路复用的,即一个网络连接,无法重复发信息,连接是一次性的。关键都在于我们如何使用 listen
和 accept
函数来配合核心的负责 I/O 的 recv
和 send
函数。