关于计算机网络以及Socket编程相关的概念性知识,可以参考相关资料,这里只列举了常见函数的用法,以及两个简单的例子。

Socket 相关函数

socket 函数

创建socket(套接字)

1
2
3
#include <sys/socket.h>

int socket(int family, int type, int protocol);

参数:

  • family:协议族,通常取 AF_INET(IPv4) 或 AF_INET6(IPv6)
  • type:套接字类型,通常取 SOCK_STREAM(TCP) 或 SOCK_DGRAM(UDP)
  • protocol:协议,可以取IPPROTO_TCPIPPTOTO_UDP,但是更建议直接取0,表示自动使用默认的协议

返回值:

  • 创建成功:返回一个新的套接字文件描述符sockfd
  • 创建失败:返回 -1,通过errno获取错误信息

因此最常见的两种用法以及错误处理如下

1
2
3
4
5
6
7
8
9
10
11
12
13
// TCP
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("Socket creation failed");
return -1;
}

// UDP
int sockfd = socket(AF_INET6, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("Socket creation failed");
return -1;
}

sockaddr 结构体

在socket网络通讯中,需要定义一个通用的sockaddr结构体来保存对方的地址信息,sockaddr定义如下

1
2
3
4
5
6
#include <sys/socket.h>

struct sockaddr {
sa_family_t sa_family; // 地址族(2 字节)
char sa_data[14]; // 具体协议的地址信息(14 字节)
};

但是上述结构体太通用了,实际上我们对于IPv4使用如下更具体的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <netinet/in.h>

struct sockaddr_in {
sa_family_t sin_family; // 地址族(2 字节)
in_port_t sin_port; // 端口号(2 字节)
struct in_addr sin_addr; // IPv4 地址(4 字节)
char sin_zero[8]; // 填充字节,以保证和 sockaddr 大小一致
};

// IPv4 地址结构体(4 字节)
struct in_addr {
uint32_t s_addr; // IPv4 地址(4 字节)
};

其中:

  • sin_family:地址族,通常取 AF_INET(IPv4)或 AF_INET6(IPv6)
  • sin_port:端口号,通常取大于1024且小于65536的整数,过小的端口号会被系统自动分配,或者需要sudo权限。如果将端口设置为0,表示使用随机端口,一般不推荐
  • sin_addr.s_addr:IPv4 地址,例如可以取127.0.0.1(本地地址),0.0.0.0(任意地址,可以使用INADDR_ANY),或者某个具体的IPv4地址
  • sin_zero:填充字节,以保证和 sockaddr 大小一致,不需要设置

对于IPv6来说,需要的结构体略有不同,这里不再赘述。在传递给bindconnect等API时,通常会将sockaddr_in强制转换成sockaddr类型。

在创建socketaddr_in结构体时,通常需要使用大小端转换和IP地址的格式转换,下面提供几个常见的例子。

服务端使用,指定任意 IP 的 PORT 端口

1
2
3
4
5
6
7
#define PORT 8080

struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr)); // 清空结构体
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_port = htons(PORT); // 端口号(主机字节序转换为网络字节序)
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用 IP 地址

客户端使用,指定 SERVER_IP 的 PORT 端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define SERVER_IP "127.0.0.1"
#define PORT 8080

struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr)); // 清空结构体
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_port = htons(PORT); // 端口号

// IPv4 地址格式转换并设置
if(inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0){
perror("Invalid address");
// close(sock);
return -1;
}

bind 函数

bind 可以用于绑定套接字到指定的地址(IP 地址和端口号),用于服务器端的监听(包括TCP和UDP的服务端)。

1
2
3
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数:

  • sockfd:套接字描述符
  • addr:指向 sockaddr 结构体 的指针,通常由 sockaddr_in 结构体转换
  • addrlenaddr的长度,一般用 sizeof 获取

返回值:

  • 成功:返回 0
  • 失败:返回 -1,通过errno获取错误信息

使用以及错误处理例如

1
2
3
4
5
if (bind(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("Binding failed");
close(sock);
return -1;
}

listen 函数

对于TCP服务端,在bind之后可以使用listen函数开启监听,也就是将套接字从CLOSE状态转换为LISTEN状态。

1
2
3
#include <sys/socket.h>

int listen(int sockfd, int backlog);

参数:

  • sockfd:要操作的socket
  • backlog:为服务器处于LISTEN状态下维护的已完成连接队列长度的最大值,例如5或者10(当然在系统中也存在一个上限)

返回值:

  • 成功:返回 0
  • 失败:返回 -1,通过errno获取错误信息

使用以及错误处理例如

1
2
3
4
if (listen(sockfd, 5) == -1) {
perror("Listen failed");
return -1;
}

connect 函数

connect 函数用于客户端主动连接到服务器,通常在TCP客户端的连接过程中使用(UDP也可以使用,但是主要适用于TCP)

1
2
3
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数:

  • sockfd:套接字描述符
  • addr:指向 sockaddr 结构体 的指针,通常由 sockaddr_in 结构体转换
  • addrlenaddr的长度,一般用 sizeof 获取

返回值:

  • 成功:返回 0
  • 失败:返回 -1,通过errno获取错误信息

使用以及错误处理例如

1
2
3
4
5
if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
perror("Connect failed");
close(sockfd);
return -1;
}

accept 函数

当TCP服务端调用 bind 和 listen 之后,服务器会在 accept 处阻塞,等待客户端发起连接,在连接成功后返回新的 socket 用于通信,并且可以通过输出参数获取客户端连接信息。(因为accept存在阻塞行为,TCP服务器可以使用fork多进程的方式来实现同时处理多个客户端连接,但是UDP服务器是不需要的)

1
2
3
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数:

  • sockfd:套接字描述符
  • addr:(输出参数)指向 sockaddr 结构体 的指针,通常由 sockaddr_in 结构体转换,可填 NULL,表示不获取
  • addrlen:(输出参数)指向addr长度的指针,可填 NULL,表示不获取

返回值:

  • 成功:返回新的socket(注意这里socket也需要手动关闭)
  • 失败:返回 -1,通过errno获取错误信息

使用以及错误处理例如

1
2
3
4
5
6
7
8
9
10
11
12
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);

// 阻塞,等待连接
int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);
if (client_fd == -1) {
perror("Accept failed");
// ...
}

// 打印客户端的 IP
printf("Client connected: %s\n", inet_ntoa(client_addr.sin_addr));

不获取客户端信息时,可以更加简单

1
2
3
4
5
int client_fd = accept(server_fd, NULL, NULL);
if (client_fd == -1) {
perror("Accept failed");
// ...
}

close 函数

close函数可以用于关闭套接字,就像文件关闭一样。

1
2
3
#include <unistd.h>

int close(int fd);

注意:

  • 由于socket允许复制,close操作只是减少socket的引用计数,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
  • 由于异常退出可能造成资源泄露,我们可能需要捕获ctrl+c等触发的异常信号,以便及时释放资源。

Socket IO 函数

网络IO操作有下面几组函数:

  • write / read
  • writev / readv
  • send / recv
  • sendto / recvfrom
  • sendmsg / recvmsg

前两组是通用的IO操作,可以用于文件读写等,第二组是适合多个缓冲区的IO操作,自动针对多个缓冲区进行,多个缓冲区按照顺序连接。 这些操作也支持socket的读写操作,但是通常不适用于UDP,这里不做讨论。 我们重点关注后三组函数。

需要说明的是, 考虑到缓冲区大小限制和网络原样,发送和接收数据的时候,无法保证不会一次性发送和接收完全部数据,可能需要套一层循环,但是这里简化讨论,不考虑这个问题。

send 和 recv 函数

这两个函数是最基础的IO函数,通常只用于TCP,因为TCP是有连接的。

1
2
3
#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数:

  • sockfd:套接字描述符
  • buf:发送数据缓冲区
  • len:发送数据缓冲区长度
  • flags:标志位,用于控制读写行为,一般取0即可,也可使用 MSG_* 标志位

返回值:

  • 成功:返回实际发送的字节数
  • 连接关闭:返回 0
  • 发送失败:返回 -1,通过errno获取错误信息

使用示例

1
2
3
4
5
ssize_t sent_bytes = send(sockfd, buffer, strlen(buffer), 0);
if (sent_bytes < 0) {
perror("send failed");
break;
}

1
2
3
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数:

  • sockfd:套接字描述符
  • buf:(输出参数)接收数据缓冲区
  • len:接收数据缓冲区长度(最大可接收的字节数)
  • flags:标志位,用于控制读写行为,一般取0即可,也可使用 MSG_* 标志位

返回值:

  • 成功:返回实际接收的字节数(可能小于len
  • 连接关闭:返回 0
  • 发送失败:返回 -1,通过errno获取错误信息

使用示例

1
2
3
4
5
6
7
8
ssize_t recv_bytes = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (recv_bytes < 0) {
perror("recv failed");
break;
} else if (recv_bytes == 0) {
printf("Server closed connection.\n");
break;
}

sendto 和 recvfrom 函数

这两个函数只用于UDP,由于UDP通常是无连接的,在读写中都加入了dest_addraddrlen

1
2
3
4
#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);

参数:

  • sockfd:套接字描述符
  • buf:发送数据缓冲区
  • len:发送数据缓冲区长度
  • flags:标志位,用于控制读写行为,一般取0即可,也可使用 MSG_* 标志位
  • dest_addr:接收方地址
  • addrlen:接收方地址长度

返回值:

  • 成功:返回实际发送的字节数
  • 发送失败:返回 -1,通过errno获取错误信息

使用例如

1
2
3
4
5
6
7
ssize_t sent_len = sendto(sockfd, message, strlen(message), 0,
(struct sockaddr *)&server_addr, sizeof(server_addr));
if (sent_len < 0) {
perror("sendto failed");
close(sockfd);
exit(EXIT_FAILURE);
}

1
2
3
4
#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);

参数:

  • sockfd:套接字描述符
  • buf:(输出参数)接收数据缓冲区
  • len:接收数据缓冲区长度(最大可接收的字节数)
  • flags:标志位,用于控制读写行为,一般取0即可,也可使用 MSG_* 标志位
  • src_addr:(输出参数)发送方地址
  • addrlen:(输出参数)发送方地址长度

返回值:

  • 成功:返回实际接收的字节数(可能小于len
  • 连接关闭:返回 0
  • 发送失败:返回 -1,通过errno获取错误信息

使用示例

1
2
3
4
5
6
7
ssize_t recv_len = recvfrom(sockfd, buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&client_addr, &addr_len);
if (recv_len < 0) {
perror("recvfrom failed");
close(sockfd);
exit(EXIT_FAILURE);
}

sendmsg 和 recvmsg 函数

最后的这两个函数更具有一般性,行为更加灵活,实际上可以完全替代前两组函数,因为它引入了struct msghdr这个结构体参数。

1
2
3
#include <sys/socket.h>

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

参数:

  • sockfd:套接字描述符
  • msg:消息结构体,打包了所有信息
  • flags:标志位,用于控制读写行为,一般取0即可,也可使用 MSG_* 标志位

返回值:

  • 成功:返回实际发送的字节数
  • 连接关闭:返回 0
  • 失败:返回 -1,通过 errno 获取错误信息
1
2
3
#include <sys/socket.h>

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

参数:

  • sockfd:套接字描述符
  • msg:(输出参数)消息结构体,打包了所有信息
  • flags:标志位,用于控制读写行为,一般取0即可,也可使用 MSG_* 标志位

返回值:

  • 成功:返回实际接收的字节数
  • 连接关闭:返回 0
  • 失败:返回 -1,通过 errno 获取错误信息

这里我们需要关注 iovecmsghdr 结构体的具体内容,iovec 结构体定义如下

1
2
3
4
5
6
#include <sys/uio.h>

struct iovec {
void *iov_base; // 缓冲区的起始地址
size_t iov_len; // 缓冲区的大小(字节数)
};

使用例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 发送数据准备
struct iovec iov[2];
iov[0].iov_base = "Hello, ";
iov[0].iov_len = 7;
iov[1].iov_base = "World!";
iov[1].iov_len = 6;

// 接收数据准备
struct iovec iov[2];
char buf1[10];
char buf2[10];

iov[0].iov_base = buf1;
iov[0].iov_len = sizeof(buf1);
iov[1].iov_base = buf2;
iov[1].iov_len = sizeof(buf2);

msghdr 结构体定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <sys/socket.h>

struct msghdr {
void *msg_name; // 地址结构体指针(仅用于 UDP)
socklen_t msg_namelen; // 地址结构体的大小(仅用于 UDP)

struct iovec *msg_iov; // 指向 iovec 数组的指针
size_t msg_iovlen; // iovec 数组的元素个数

void *msg_control; // 指向额外控制信息的指针(如文件描述符)
size_t msg_controllen; // 额外控制信息数据大小
int msg_flags; // 额外标志位
};

其中的很多成员都是可选的,TCP只需要关注核心的msg_iov以及msg_iovlen即可,UDP还需要关注msg_namemsg_namelen

TCP使用例如

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
// 发送准备
struct iovec iov[2];
char part1[] = "Hello, ";
char part2[] = "this is TCP message.\n";
iov[0].iov_base = part1;
iov[0].iov_len = strlen(part1);
iov[1].iov_base = part2;
iov[1].iov_len = strlen(part2);

struct msghdr msg;
memset(&msg, 0, sizeof(msg));
msg.msg_iov = iov;
msg.msg_iovlen = 2;

// 接收准备
struct iovec iov[2];
char buf1[16];
char buf2[32];
iov[0].iov_base = buf1;
iov[0].iov_len = sizeof(buf1);
iov[1].iov_base = buf2;
iov[1].iov_len = sizeof(buf2);

struct msghdr msg;
memset(&msg, 0, sizeof(msg));
msg.msg_iov = iov;
msg.msg_iovlen = 2;

UDP使用例如

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
// 发送准备
struct iovec iov[2];
char part1[] = "UDP message: ";
char part2[] = "Hello!";
iov[0].iov_base = part1;
iov[0].iov_len = strlen(part1);
iov[1].iov_base = part2;
iov[1].iov_len = strlen(part2);

struct msghdr msg;
memset(&msg, 0, sizeof(msg));
msg.msg_name = &server_addr;
msg.msg_namelen = sizeof(server_addr);
msg.msg_iov = iov;
msg.msg_iovlen = 2;

// 接收准备
struct iovec iov[1];
char buf[64];
iov[0].iov_base = buf;
iov[0].iov_len = sizeof(buf);

struct msghdr msg;
memset(&msg, 0, sizeof(msg));
msg.msg_name = &client_addr;
msg.msg_namelen = sizeof(client_addr);
msg.msg_iov = iov;
msg.msg_iovlen = 1;

实例——echo服务器

我们考虑实现一个最简单的echo服务器,分别通过TCP和UDP实现。 echo服务器的逻辑很简单,将收到的信息原样发送回去,这里我们顺便在发送信息时附带了时间,在返回信息中加上了双方的地址信息。

由于TCP的accpet函数存在阻塞,使用fork多进程的方式来实现同时处理多个客户端连接。

TCP 服务端

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
#include <arpa/inet.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define PORT 8081
#define BUFFER_SIZE 1024

int server_fd;

// 处理 SIGINT 关闭服务器
void handle_sigint(int sig) {
close(server_fd);
write(STDOUT_FILENO, "\nServer shutting down...\n", 25);
exit(0);
}

// 处理 SIGCHLD 避免僵尸进程
void handle_sigchld(int sig) {
while (waitpid(-1, NULL, WNOHANG) > 0) {}
}

// 处理客户端连接
void handle_client(int client_fd) {
char buffer[BUFFER_SIZE];
ssize_t bytes_read = 0;

// 获取客户端地址信息
struct sockaddr_in client_addr;
socklen_t addr_len1 = sizeof(client_addr);
getpeername(client_fd, (struct sockaddr *)&client_addr, &addr_len1);
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
int client_port = ntohs(client_addr.sin_port);

// 获取服务器地址信息
struct sockaddr_in server_addr;
socklen_t addr_len2 = sizeof(server_addr);
getsockname(client_fd, (struct sockaddr *)&server_addr, &addr_len2);
char server_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &server_addr.sin_addr, server_ip, sizeof(server_ip));
int server_port = ntohs(server_addr.sin_port);

while ((bytes_read = recv(client_fd, buffer, BUFFER_SIZE, 0)) > 0) {
buffer[bytes_read] = '\0';

// 生成返回信息
char response[BUFFER_SIZE + 100];
snprintf(response, sizeof(response),
"[Server %s:%d] Received from [Client %s:%d]:\n%s", server_ip,
server_port, client_ip, client_port, buffer);

// 打印到服务器终端
write(STDOUT_FILENO, response, strlen(response));

// 发送回客户端
send(client_fd, response, strlen(response), 0);
}

write(STDOUT_FILENO, "Client disconnected.\n", 21);
close(client_fd);
exit(0);
}

int main() {
// 处理 Ctrl+C 终止
signal(SIGINT, handle_sigint);

// 处理子进程退出
signal(SIGCHLD, handle_sigchld);

// 创建 TCP 套接字
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}

// 配置服务器地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);

// 绑定到端口
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr))
< 0) {
perror("Bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}

// 开始监听
if (listen(server_fd, 5) < 0) {
perror("Listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}

write(STDOUT_FILENO,
"TCP Echo server binding and listening on port 8080...\n", 55);

while (1) {
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);

int client_fd =
accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
if (client_fd < 0) {
perror("Accept failed");
continue;
}

// 获取客户端 IP 和端口
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
int client_port = ntohs(client_addr.sin_port);

char msg[100];
snprintf(msg, sizeof(msg), "New client connected: %s:%d\n", client_ip,
client_port);
write(STDOUT_FILENO, msg, strlen(msg));

pid_t pid = fork();
if (pid == 0) { // 子进程关闭server_fd,处理client_fd
close(server_fd);
handle_client(client_fd);
}
else if (pid > 0) { // 父进程关闭client_fd
close(client_fd);
}
else { perror("Fork failed"); }
}

close(server_fd);
return 0;
}

UDP 服务端

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
#include <arpa/inet.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

#define PORT 8081
#define BUFFER_SIZE 1024

int server_fd;

// 处理 SIGINT 关闭服务器
void handle_sigint(int sig) {
close(server_fd);
write(STDOUT_FILENO, "\nServer shutting down...\n", 25);
exit(0);
}

int main() {
// 处理 Ctrl+C 终止
signal(SIGINT, handle_sigint);

// 创建 UDP 套接字
server_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (server_fd < 0) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}

// 配置服务器地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);

// 绑定到端口
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr))
< 0) {
perror("Bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}

write(STDOUT_FILENO, "UDP Echo server binding on port 8080...\n", 41);

char buffer[BUFFER_SIZE];
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);

while (1) {
// 接收数据
ssize_t bytes_received =
recvfrom(server_fd, buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&client_addr, &addr_len);
if (bytes_received < 0) {
perror("Receive failed");
continue;
}
buffer[bytes_received] = '\0';

// 获取客户端 IP 和端口
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
int client_port = ntohs(client_addr.sin_port);

// 获取服务器 IP 和端口
struct sockaddr_in server_info;
socklen_t server_len = sizeof(server_info);
getsockname(server_fd, (struct sockaddr *)&server_info, &server_len);
char server_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &server_info.sin_addr, server_ip, sizeof(server_ip));
int server_port = ntohs(server_info.sin_port);

// 生成返回信息
char response[BUFFER_SIZE + 100];
snprintf(response, sizeof(response),
"[Server %s:%d] Received from [Client %s:%d]:\n%s", server_ip,
server_port, client_ip, client_port, buffer);

// 打印到服务器终端
write(STDOUT_FILENO, response, strlen(response));

// 发送回客户端
sendto(server_fd, response, strlen(response), 0,
(struct sockaddr *)&client_addr, addr_len);
}

close(server_fd);
return 0;
}

客户端

使用 TCP 和 UDP 的客户端代码非常相似,因此写在了一起,使用USE_TCP宏来区分

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
#include <arpa/inet.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8081
#define BUFFER_SIZE 1024

int sockfd;

// 处理 Ctrl+C 关闭客户端
void handle_sigint(int sig) {
close(sockfd);
write(STDOUT_FILENO, "\nClient shutting down...\n", 25);
exit(0);
}

// 获取格式化的当前时间
void get_timestamp(char *buffer, size_t size) {
time_t now = time(NULL);
struct tm *tm_info = localtime(&now);
strftime(buffer, size, "[%H:%M:%S] ", tm_info);
}

int main() {
// 处理 Ctrl+C
signal(SIGINT, handle_sigint);

// 创建套接字
#ifdef USE_TCP
sockfd = socket(AF_INET, SOCK_STREAM, 0);
#else
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
#endif
if (sockfd < 0) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}

// 配置服务器地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);

#ifdef USE_TCP
// 连接到服务器
if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr))
< 0) {
perror("Connection failed");
close(sockfd);
exit(EXIT_FAILURE);
}
#else
// do nothing
#endif

write(STDOUT_FILENO, "Connected to server. Type your messages...\n", 44);

char buffer[BUFFER_SIZE];
char send_buffer[BUFFER_SIZE + 20]; // 额外空间存放时间戳
#ifdef USE_TCP
// do nothing
#else
struct sockaddr_in from_addr;
socklen_t from_len = sizeof(from_addr);
#endif

while (1) {
write(STDOUT_FILENO, "You: ", 5);

// 读取用户输入
if (fgets(buffer, BUFFER_SIZE, stdin) == NULL) {
write(STDOUT_FILENO, "\nClient exiting...\n", 19);
break;
}

// 获取时间戳并拼接
get_timestamp(send_buffer, sizeof(send_buffer));
strncat(send_buffer, buffer, BUFFER_SIZE);

#ifdef USE_TCP
// 发送数据
send(sockfd, send_buffer, strlen(send_buffer), 0);

// 接收回应
ssize_t bytes_received = recv(sockfd, buffer, BUFFER_SIZE, 0);
#else
// 发送数据
sendto(sockfd, send_buffer, strlen(send_buffer), 0,
(struct sockaddr *)&server_addr, sizeof(server_addr));

// 接收回应
ssize_t bytes_received =
recvfrom(sockfd, buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&from_addr, &from_len);

#endif
if (bytes_received > 0) {
buffer[bytes_received] = '\0';
write(STDOUT_FILENO, "Server: ", 8);
write(STDOUT_FILENO, buffer, bytes_received);
}
else {
write(STDOUT_FILENO, "Server disconnected.\n", 21);
break;
}
}

close(sockfd);
return 0;
}

实例——极简http服务器

这个极简的http服务器的内容很简单:

  • GET 请求:
    • 主页 (/):显示欢迎信息,并提供一个跳转到 /ask 页面的链接。
    • 问答页 (/ask):包含一个 HTML 表单,允许用户输入内容并提交。
    • 404 处理:对于未知路径,服务器返回 404 Not Found。
  • 来自问答页的 POST 处理:用户提交的内容将被服务器接收,并显示在返回的 HTML 页面中。

http服务器就不需要客户端了,直接使用浏览器访问即可。

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
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define PORT 8081
#define BUFFER_SIZE 1024

// HTML 页面
const char *PAGE_INDEX = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/html\r\n"
"Connection: close\r\n\r\n"
"<html><body><h1>Welcome to the Home Page</h1>"
"<a href='/ask'>Go to Ask Page</a></body></html>";

const char *PAGE_ASK = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/html\r\n"
"Connection: close\r\n\r\n"
"<html><body>"
"<h1>Ask Something</h1>"
"<form action='/ask' method='POST'>"
"<input type='text' name='question'/>"
"<input type='submit' value='Ask'/>"
"</form></body></html>";

const char *PAGE_NOT_FOUND = "HTTP/1.1 404 Not Found\r\n"
"Content-Type: text/html\r\n"
"Connection: close\r\n\r\n"
"<html><body><h1>404 Not Found</h1></body></html>";

// 处理 HTTP 请求
void handle_request(int client_fd) {
char buffer[BUFFER_SIZE];
ssize_t bytes_read = read(client_fd, buffer, sizeof(buffer) - 1);
if (bytes_read <= 0) {
close(client_fd);
return;
}
buffer[bytes_read] = '\0';

// 解析请求方法和路径
char method[10];
char path[100];
sscanf(buffer, "%s %s", method, path);

const char *response = NULL;

if (strcmp(method, "GET") == 0) {
if (strcmp(path, "/") == 0)
response = PAGE_INDEX;
else if (strcmp(path, "/ask") == 0)
response = PAGE_ASK;
else
response = PAGE_NOT_FOUND;
}
else if (strcmp(method, "POST") == 0 && strcmp(path, "/ask") == 0) {
// 提取 POST 数据
char *body = strstr(buffer, "\r\n\r\n");
if (body) {
body += 4; // 跳过 "\r\n\r\n"
char question[BUFFER_SIZE] = {0};
sscanf(body, "question=%s", question);

// 生成响应 HTML
char response_body[BUFFER_SIZE];
snprintf(response_body, sizeof(response_body),
"<html><body><h1>Your Question:</h1><p>%s</p>"
"<a href='/ask'>Ask Again</a></body></html>",
question);

static char response_header[BUFFER_SIZE + 100];
snprintf(response_header, sizeof(response_header),
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n"
"Connection: close\r\n\r\n%s",
response_body);
response = response_header;
}
}

if (!response) response = PAGE_NOT_FOUND;

write(client_fd, response, strlen(response));
close(client_fd);
}

int main() {
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);

// 创建 TCP 套接字
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}

// 绑定端口
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr))
< 0) {
perror("Bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}

// 监听连接
if (listen(server_fd, 5) < 0) {
perror("Listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("HTTP Server listening on port %d...\n", PORT);

while (1) {
int client_fd =
accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
if (client_fd < 0) {
perror("Accept failed");
continue;
}
handle_request(client_fd);
}

close(server_fd);
return 0;
}

这里还有一个稍微复杂一点的http服务器:Tinyhttpd, 这是J. David Blackstone在1999年写的一个不到 500 行的超轻量型 Http Server,读一遍可以更好地理解http服务器的原理。 它除了可以根据请求返回本地目录中的文件内容,还支持 CGI:从请求中提取参数并传递给脚本执行,脚本计算并生成html返回。

补充

大小端数值转换

首先,不同的机器上对于多字节变量的字节存储顺序是不同的,有大端字节序和小端字节序两种,目前常见的机器都是小端序,但是在网络编程中统一使用大端序,也成为网络字节序。

在Linux中,提供了四个用于主机字节序和网络字节序之间相互转换的函数:

1
2
3
4
5
6
7
8
9
10
11
#include <netinet/in.h>

// host to network (short)
uint16_t htons(uint16_t value);
// host to network (long)
uint32_t htonl(uint32_t value);

// network to host (short)
uint16_t ntohs(uint16_t value);
// network to host (long)
uint32_t ntohl(uint32_t value);

IP地址格式转换

IP地址一共有两种格式:

  • Presentation(表达格式):也就是我们能看得懂的格式,例如"192.168.19.12"这样的字符串
  • Numeric(数值格式):可以存入套接字地址结构体的格式,数据类型为整型

Linux 提供了两个函数用于IP地址格式的相互转换的函数:

  • inet_pton():将IP地址从表达格式转换为数值格式
  • inet_ntop():将IP地址从数值格式转换为表达格式
1
2
3
#include <arpa/inet.h>

int inet_pton(int family, const char *strptr, void *addrptr);

参数:

  • family:地址族,通常取 AF_INET(IPv4)或 AF_INET6(IPv6)。
  • strptr:(输入参数,表达格式)指向以点分隔的十进制(IPv4)或冒号分隔(IPv6)的 IP 地址字符串的字符指针。
  • addrptr:(输出参数,数值格式)指向 struct in_addr(IPv4)或 struct in6_addr(IPv6)的通用指针,用于存储转换后的二进制 IP 地址。

返回值:

  • 如果转换成功,则返回 1。
  • 如果表达格式的 IP 地址格式有误,则返回 0。
  • 如果转换出错则返回 -1,并设置相应的 errno
1
2
3
#include <arpa/inet.h>

const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);

参数:

  • family:地址族,AF_INET(IPv4)或 AF_INET6(IPv6)。
  • addrptr:(输入参数,数值格式)指向 struct in_addr(IPv4)或 struct in6_addr(IPv6)的通用指针。
  • strptr:(输出参数,表达格式)用于存储转换后的 IP 地址字符串的缓冲区字符指针。
  • lenstrptr 的缓冲区长度。

返回值:

  • 如果转换成功,则返回 strptr 指针。
  • 如果出错,则返回 NULL,并设置相应的 errno

输出缓冲区的大小建议设置为如下值

1
2
3
4
#include <netinet/in.h>

#define INET_ADDRSTRLEN 16 // IPv4地址的表达格式的长度
#define INET6_ADDRSTRLEN 46 // IPv6地址的表达格式的长度