socket网络编程

OSI七层网络模型

先了解网络模型,更有利于后续学习和理解。

七层网络模型,实际上可以简化到四层(TCP/IP模型)。

网络模型
(图片来源:C语言中文网-socket,如果是从基础学习,推荐看看大佬的文章)

需要注意的是,数据只能逐层传输不能跃层,每一层都可以使用下层提供的服务,并为上层提供服务。

了解这个之后,我们对开发中不理解的地方,先搞清楚数据传输经历了哪些层或许更好理解为什么代码那么写。

TCP/IP协议族

上面图其实已经标注了TCP/IP模型包含四层,所谓简化就是简化成TCP/IP模型。

TCP/IP 模型包含了 TCP、IP、UDP、Telnet、FTP、SMTP 等上百个互为关联的协议,其中 TCP 和 IP 是最常用的两种底层协议,所以把它们统称为“TCP/IP 协议族”。

注意,是“TCP/IP模型”包含很多协议,我们把这些协议统称为“TCP/IP协议族”。

本文记录socket编程也是基于TCP和UDP协议的,说“也是”,是因为大部分文章,大部分人都是从TCP/UDP记录学习的。

TCP和UDP协议的层级关系如下

TCP和UDP协议的层级关系

socket套接字类型

面向连接的套接字和无连接的套接字,专业点叫流格式套接字(SOCK_STREAM)和数据报格式套接字(SOCK_DGRAM)。

SOCK_STREAM,底层基于TCP协议,提供一种可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。

SOCK_DGRAM,底层基于UDP协议,是一种不可靠的、不按顺序传递的、以追求速度为目的的套接字。

对比维度 面向连接(TCP) 无连接(UDP)
连接建立 必须先三次握手建立专属连接,通信结束四次挥手断开 无需建立连接,直接发数据包,随时收发
可靠性 可靠:丢包重传、差错校验、应答确认 不可靠:不保证送达、不纠错、丢包不重发
数据有序性 保证按发送顺序到达,无乱序 可能乱序、重复,数据包独立无关联
数据形式 字节流(无数据边界,会粘包) 独立数据报(有边界,一次发送对应一次接收)
拥塞 / 流量控制 自带拥塞控制、滑动窗口流量控制 无任何控制,发送过快会直接丢包
头部开销 TCP 头部大(20~60 字节),开销高 UDP 头部极小(8 字节),转发快、延迟低
通信模式 一对一点对点通信 支持一对一、广播、组播
编程流程 复杂:listen/accept/connect 缺一不可 简单:无需监听、连接,直接 sendto/recvfrom

PS: Linux 下可以用read/recv,Windows 下使用recv/recvfrom

TCP 基于字节流传输,没有数据边界,会出现粘包(多包合并)、拆包(一包拆分)。简单解决思路:

  • 固定长度:收发约定相同消息长度
  • 分隔符结尾:用特殊符号标记消息结束
  • 包头 + 长度:消息前加长度字段,按长度读取

面向连接(TCP)流程

服务端:
socket() → bind() → listen() → accept() → send()/recv() → close()

客户端:
socket() → connect() → send()/recv() → close()

无连接(UDP)流程

服务端 / 客户端通用:
socket() → bind(可选) → sendto()/recvfrom()

IP、MAC和端口号

公网 IP 可以定位到一台主机(或 NAT 网关),但内网 IP 需要结合 NAT 才能唯一标识终端。在互联网上跑数据的IP都是公网IP,能定位到一个路由器,但一个路由器下还有局域网,并不能确定数据报来自哪个用户。

只有MAC地址能唯一确定一台计算机(网卡),每张网卡的MAC都是独一无二的,虽然也有修改MAC地址的技术手段,但我们讨论正常情况。

端口号是网络程序的网络接口。前面说了MAC能确定计算机,那还要精确到数据报来自计算机上的哪个网络程序,就要看端口号了,系统会为每个开启的网络应用分配一个端口号,我们自己写网络应用也可以指定端口号,但需要注意0-1024是知名端口,避开他们。

有IP地址、MAC地址和端口号,才能唯一确定是哪个网络应用要通信。

阻塞与唤醒

我之所以用阻塞与唤醒的标题,是因为这里的阻塞实际上是休眠。

以前我只知道阻塞是应用程序看起来卡住,之后触发什么条件从而继续运行的这么一个行为表现,但实际上这个话题比我想象的高深得多。

首先说明,我们可以用 fcntl/ioctl 把 Socket 设置为非阻塞模式,但默认情况我们socket编程处于阻塞模式。

当我们编写基于tcp的服务端时,调用 accept 就会阻塞,我好奇过系统是怎么知道我客户端发了消息服务端就知道继续往下执行的,一开始我以为是accept内部在轮询检查,不断循环检查是不是有消息,这样子处理的,但实际上,我的想法过于幼稚。

阻塞调用(accept/recv/read/epoll_wait)会把当前线程彻底移出 CPU 就绪 / 运行任务队列,进入休眠等待队列,完全不占用CPU资源;这实际上对了解操作系统的开发者而言很容易想到,只在应用程序层面考虑可能不会太深。

既然是休眠,那与之对应的就是唤醒了。唤醒靠的是硬件中断+事件驱动,当客户端发送信息之后,服务端网卡收到信号触发硬件中断,把消息写入内核缓冲区,接着内核标fd就绪把fd放进epoll就绪链表,再之后内核把之前休眠挂起的服务端线程,重新丢回 CPU 执行调度队列,最后,线程分到 CPU 时间片 → 阻塞函数返回,业务代码接着跑。

epoll 的事件通知机制与 select 不同,前者在内核维护就绪链表,后者是每次扫描(在后面的示例有所体现),所以epoll比select更高效;阻塞 I/O 时进程状态为 TASK_INTERRUPTIBLE(可中断睡眠),可以被信号唤醒。

客户端与服务端示例(Linux && TCP)

客户端由于是通用的就不过多解释,主要解释服务端。

把服务端 = 奶茶店店员,客户端 = 客人

  • 单线程阻塞:店员一次只给一个客人做奶茶,其他人站在门口等死,后面的人根本进不来(玩具项目,只能连接一个客户端);
  • 多线程:来一个客人雇一个店员,店里挤满店员,工资(内存)付不起,店直接倒闭(开辟线程服务,本质还是一对一,占用资源);
  • select/epoll:一个店员盯着所有客人,谁举手说「我要下单 / 我要取餐」,店员就立刻过去服务,效率拉满(内核调度)。

总结一下

不用 select/epoll
服务端要么只能连 1 个客户端,要么线程太多把服务器干崩;
用 select/epoll
单线程同时管理海量连接,实现高并发;

用最低的资源,处理最多的客户端,这就是网络服务器的核心。

下面看看echo服务端和客户端的具体实现,客户端发送消息给服务端,服务端原封不动把客户端发送的消息返回。

通用客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8888
#define BUFFER_SIZE 1024

int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char send_buffer[BUFFER_SIZE] = {0};
char buffer[BUFFER_SIZE] = {0};

if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("socket 创建失败\n");
return -1;
}

serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);

// 将服务器 IP 地址从点分十进制转换为二进制
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
printf("地址无效\n");
return -1;
}

if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
printf("连接失败\n");
return -1;
}
printf("成功连接服务端!输入消息发送,输入 exit 退出\n");

// 发送消息
while (1) {
// 清空缓冲区
memset(send_buffer, 0, BUFFER_SIZE);
memset(buffer, 0, BUFFER_SIZE);

// 从控制台读取输入
printf("请输入消息:");
fgets(send_buffer, BUFFER_SIZE, stdin);

// 去掉fgets自动读取的换行符
send_buffer[strcspn(send_buffer, "\n")] = '\0';

// 输入exit,退出循环
if (strcmp(send_buffer, "exit") == 0) {
printf("客户端退出\n");
break;
}

// 发送消息
send(sock, send_buffer, strlen(send_buffer), 0);

// 接收服务端回显
int len = read(sock, buffer, BUFFER_SIZE);
if (len <= 0) {
printf("服务端断开连接\n");
break;
}
printf("服务端回显: %s\n", buffer);
}

close(sock);
return 0;
}

单连接服务端(玩具)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8888
#define BUFFER_SIZE 1024

int main() {
int server_fd, client_fd;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
int addr_len = sizeof(serv_addr);

// 1. 创建服务端socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket 创建失败");
exit(EXIT_FAILURE);
}

// 端口复用(解决重启报错问题)
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(PORT);

// 2. 绑定端口
if (bind(server_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("绑定失败");
exit(EXIT_FAILURE);
}

// 3. 监听
if (listen(server_fd, 3) < 0) {
perror("监听失败");
exit(EXIT_FAILURE);
}
printf("服务端启动成功,监听端口 %d,等待客户端连接...\n", PORT);

// 4. 接收客户端连接
if ((client_fd = accept(server_fd, (struct sockaddr *)&serv_addr, (socklen_t*)&addr_len)) < 0) {
perror("接收连接失败");
exit(EXIT_FAILURE);
}
printf("客户端已连接!\n");

// 5. 循环接收消息 + 回显(核心逻辑)
while (1) {
memset(buffer, 0, BUFFER_SIZE);
// 读取客户端消息
int len = read(client_fd, buffer, BUFFER_SIZE);

// 客户端断开或出错
if (len <= 0) {
printf("客户端断开连接\n");
break;
}

printf("收到客户端消息: %s\n", buffer);

// 回显给客户端
send(client_fd, buffer, strlen(buffer), 0);
}

// 6. 关闭套接字
close(client_fd);
close(server_fd);
return 0;
}

IO复用多连接服务端(select)

使用select,老式方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// select
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/select.h>

#define PORT 8888
#define MAX_CLIENTS 10
#define BUFFER_SIZE 1024

// 使用 select 实现多客户端 echo 服务器
// 缺点:一般最多也就1024个fd,还要遍历检查,每次都要将整个fd_set从用户态拷贝到内核态
int main(int argc, char* argv[]) {
int server_fd, new_socket, client_socket[MAX_CLIENTS], max_sd, sd, activity, valread;
struct sockaddr_in address;
fd_set readfds;
char buffer[BUFFER_SIZE];

for (int i = 0; i < MAX_CLIENTS; ++i) client_socket[i] = 0;

// 创建新监听套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(1);
}
// 允许端口复用,tcp连接断开后不必强制等待1-2分钟再用同一端口开启服务端,可以立刻开
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(address));

address.sin_family = AF_INET;
address.sin_port = htons(PORT);
address.sin_addr.s_addr = INADDR_ANY; // 监听所有地址8888端口
bind(server_fd, (struct sockaddr*)&address, sizeof(address));
listen(server_fd, 3); // 等待区至多3个客户端连接,多出则拒绝连接,accept之后等待区才会空闲(一般写128,1024大值)
printf("服务器监听端口 %d\n", PORT);

while (1) {
// FD_ZERO(&readfds) 把所有开关全部关掉,所有位 → 0
// FD_SET(fd, &readfds) 把编号为 fd 的那个开关打开,第 fd 位 → 1
// FD_ISSET(fd, &readfds) 看看编号 fd 的开关是不是打开的,判断第 fd 位是否为 1
FD_ZERO(&readfds);
FD_SET(server_fd, &readfds);
max_sd = server_fd;

// 将客户端套接字加入集合
for (int i = 0; i < MAX_CLIENTS; ++i) {
sd = client_socket[i];
if (sd > 0) FD_SET(sd, &readfds);
if (sd > max_sd) max_sd = sd;
}

// 阻塞等待活动(最后一个time_out参数为NULL)
activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);
if (activity < 0) {
perror("select error");
}

// 检查监听套接字
if (FD_ISSET(server_fd, &readfds)) {
int addrlen = sizeof(address);
new_socket = accept(server_fd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
printf("新连接: fd %d, IP %s, 端口 %d\n", new_socket, inet_ntoa(address.sin_addr), ntohs(address.sin_port));

// 将新的套接字存入数组
for (int i = 0; i < MAX_CLIENTS; ++i) {
if (client_socket[i] == 0) {
client_socket[i] = new_socket;
break;
}
}
}

// 检查客户端套接字
for (int i = 0; i < MAX_CLIENTS; ++i) {
sd = client_socket[i];
if (FD_ISSET(sd, &readfds)) {
valread = read(sd, buffer, BUFFER_SIZE);
if (valread == 0) {
struct sockaddr_in dc_address;
int dc_addrlen = sizeof(dc_address);
getpeername(sd, (struct sockaddr*)&dc_address, (socklen_t*)&dc_addrlen); // 获取已连接客户端的地址和端口号
printf("客户端断开:IP %s, 端口 %d\n", inet_ntoa(dc_address.sin_addr), ntohs(dc_address.sin_port));
close(sd);
client_socket[i] = 0;
} else {
buffer[valread] = '\0';
printf("收到:%s\n", buffer);
send(sd, buffer, strlen(buffer), 0);
}
}
}

}


return 0;
}

IO复用多连接服务端(epoll)

epoll是Linux专属的,在Windows中有另一种机制,IOCP,都是实现高并发服务的必学机制。

epoll分水平触发(LT,默认)和边缘触发(ET),意思如同单片机引脚信号是稳定电平一直触发,还是上升沿/下降沿触发,所以二者处理事务的具体实现也不一样

水平触发因为一直会有事件通知,所以照常收发数据就好,边缘触发在客户端发数据到服务端来的时候,只会调用一次通知,所以需要循环读,直到读到EAGAIN,表示数据收干净了。

先展示水平触发(LT)的具体实现。

注意,创建epoll实例:

  • epoll_create(size):旧版接口,size 参数已废弃
  • epoll_create1(0):推荐使用,现代 Linux 标准写法,参数传0即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// epoll LT
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define PORT 8888
#define MAX_EVENTS 1024

// 1)水平触发(LT)—— 默认模式(本代码)
// 缓冲区有数据就一直通知你
// 最稳定、最好用
// 适合新手
// 2)边缘触发(ET)—— 高性能(采用边缘触发需循环read/recv直到接收到EAGAIN)
// 只在状态变化时通知一次
// 必须非阻塞 + 循环读
// Nginx 用的就是 ET

// epoll_create 创建一个 epoll 实例
// epoll_ctl 向 epoll 注册(ADD) / 修改(MOD) / 删除(DEL) fd
// epoll_wait 等待就绪事件
// epoll_event 事件结构体
// EPOLLIN / EPOLLOUT 事件类型
int main(int argc, char* argv[]) {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);
bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));

listen(server_fd, 128);

// 创建epoll实例
int epoll_fd = epoll_create1(0);

// 编码和select类似,把服务器fs加入(监听新连接)
struct epoll_event event;
event.events = EPOLLIN; // 监听可读事件
event.data.fd = server_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event);

// 事件数组(fd就绪之后存在此处,对服务端来说是有新连接,对客户端是发消息)
struct epoll_event events[MAX_EVENTS];

while (1) {
// 等待就绪事件
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);

for (int i = 0; i < n; ++i) {
int fd = events[i].data.fd;

// 新客户端连接
if (fd == server_fd) {
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &len);

// 加入epoll
event.events = EPOLLIN;
event.data.fd = client_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event);

printf("新客户端连接:fd %d\n", client_fd);
} else {
// 客户端发来数据
char buf[1024];
int n = read(fd, buf, sizeof(buf));

if (n <= 0) {
// 客户端断开
close(fd);
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
printf("客户端断开:%d\n", fd);
} else {
// 收到数据
buf[n] = '\0';
printf("收到:%s\n", buf);
// 回显
send(fd, buf, n, 0);
}
}
}
}

return 0;
}

下面是边缘触发(ET)的具体实现,可以看到在处理新连接和处理接收消息时都放在了循环while(1) {...}内,因为边缘触发的信号只会触发一次,有新连接来的时候,有一次信号,倘若此时前面客户端的连接尚未完成,他会进入等待连接的队列中,也不会再有信号了,所以我们需要循环accept,直到所有客户端完成连接;同理,收取消息也是一样,消息来的时候只会有一个信号,但我们可能没法一次收取完整的消息,剩余数据就会一直堵在缓冲区里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define PORT 8888
#define MAX_EVENTS 1024

// 1)水平触发(LT)—— 默认模式(本代码)
// 缓冲区有数据就一直通知你
// 最稳定、最好用
// 适合新手
// 2)边缘触发(ET)—— 高性能(采用边缘触发需循环read/recv直到接收到EAGAIN)
// 只在状态变化时通知一次
// 必须非阻塞 + 循环读
// Nginx 用的就是 ET

// epoll_create 创建一个 epoll 实例
// epoll_ctl 向 epoll 注册(ADD) / 修改(MOD) / 删除(DEL) fd
// epoll_wait 等待就绪事件
// epoll_event 事件结构体
// EPOLLIN / EPOLLOUT 事件类型

// 设置非阻塞模式
void set_nonblocking(int fd) {
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
}

int main(int argc, char* argv[]) {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket failed");
return -1;
}

// 端口复用
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);

if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("bind failed");
close(server_fd);
return -1;
}

if (listen(server_fd, 128) == -1) {
perror("listen failed");
close(server_fd);
return -1;
}

// 创建epoll实例
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1 failed");
close(server_fd);
return -1;
}

// 服务端fd非阻塞(ET模式必须)
set_nonblocking(server_fd);

// 添加服务端fd到epoll(ET模式)
struct epoll_event event;
event.events = EPOLLIN | EPOLLET; // 边缘触发
event.data.fd = server_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) {
perror("epoll_ctl add server failed");
close(server_fd);
close(epoll_fd);
return -1;
}

struct epoll_event events[MAX_EVENTS];
printf("Epoll ET 服务端启动成功,监听端口 %d\n", PORT);

while (1) {
// 等待事件
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (n == -1) {
perror("epoll_wait failed");
continue;
}

for (int i = 0; i < n; ++i) {
int fd = events[i].data.fd;

// 处理新连接(ET模式:必须循环accept)
if (fd == server_fd) {
// 循环accept,直到EAGAIN
while (1) {
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &len);

if (client_fd == -1) {
// 没有新连接了,退出循环
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break;
} else {
perror("accept failed");
break;
}
}

printf("新客户端连接:fd %d\n", client_fd);
set_nonblocking(client_fd);

// 添加客户端到epoll(ET模式)
event.events = EPOLLIN | EPOLLET;
event.data.fd = client_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event);
}
}

// 处理客户端消息(ET模式:循环read)
else {
char buf[1024];
while (1) {
// 每次read前清空缓冲区(防止乱码)
memset(buf, 0, sizeof(buf));
ssize_t read_len = read(fd, buf, sizeof(buf));

if (read_len > 0) {
printf("收到客户端%d数据:%s\n", fd, buf);

// send只发送有效数据长度,不发空字符
send(fd, buf, read_len, 0);
}
// 客户端断开连接
else if (read_len == 0) {
printf("客户端%d断开连接\n", fd);
// 注意此处,先删epoll,再关闭fd
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
break;
}
// 读取完毕(EAGAIN)或错误
else {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// printf("数据读取完成\n");
break;
} else {
perror("read error");
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
break;
}
}
}
}
}
}

// 资源释放(理论上到不了这里)
close(server_fd);
close(epoll_fd);
return 0;
}

客户端与服务端示例(Linux && UDP)

因为 UDP没有请求连接和受理连接(没有connect、listen和accept) 的过程,所以严格意义上来说没有办法区分服务器端和客户端,在这里只能将提供服务的称为服务器端

udp发送的每个数据包自带目标ip和端口号,使用sendto/recvfrom通信,如果在sendto之前不bind地址,第一次调用 sendto 时,内核自动做隐式Bind,内核根据路由表,自动选一个本机出口IP当源IP,自动分配一个系统临时随机端口当源端口,之后每次sendto都用这个目标地址和端口。

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// udp_server_echo.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 32
void error_handling(char* message);

int main(int argc, char** argv) {
int serv_sock;
char message[BUF_SIZE];
int str_len;
struct sockaddr_in serv_addr, clnt_addr;
socklen_t clnt_addr_size;

if (argc != 2) {
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}

serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
if (serv_sock == -1)
error_handling("UDP socket creation error!");

memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = PF_INET;
serv_addr.sin_port = htons(atoi(argv[1]));
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error!");

while (1) {
clnt_addr_size = sizeof(clnt_addr);
str_len = recvfrom(serv_sock, message, BUF_SIZE, 0,
(struct sockaddr*)&clnt_addr, &clnt_addr_size);
sendto(serv_sock, message, str_len, 0,
(struct sockaddr*)&clnt_addr, clnt_addr_size);
}

close(serv_sock);
return 0;
}

void error_handling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}

客户端

// udp_client_echo.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>      // write  read  声明的地方
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 32
void error_handling(char* message);

int main(int argc, char** argv) {
  int clnt_sock;
  char message[BUF_SIZE];
  int str_len;
  socklen_t addr_size;
  struct sockaddr_in serv_addr, from_addr;

  if (argc != 3) {
  printf("Usage: %s <ip> <port>\n", argv[0]);
  exit(1);
  }

  clnt_sock = socket(PF_INET, SOCK_DGRAM, 0);
  if (clnt_sock == -1)
  error_handling("socket() error!");

  memset(&serv_addr, 0, sizeof(serv_addr));
  serv_addr.sin_family = PF_INET;
  serv_addr.sin_port   = htons(atoi(argv[2]));
  if (inet_pton(AF_INET, argv[1], &serv_addr.sin_addr) == 0)  // 会自动转换字节序
  error_handling("inet_pton() error!");

  while(1) {
    fputs("Insert message(q to quit): ", stdout);
    fgets(message, sizeof(message), stdin);
    if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) break;
    sendto (clnt_sock, message, strlen(message), 0,
            (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    addr_size = sizeof(serv_addr);
    str_len = recvfrom(clnt_sock, message, BUF_SIZE, 0,
                (struct sockaddr*)&from_addr, &addr_size);
    message[str_len] = 0;
    printf("Message from server: %s\n", message);
    }
    close(clnt_sock);
    return 0;
  }

void error_handling(char* message) {
  fputs(message, stderr);
  fputc('\n', stderr);
  exit(1);
}