Linux环境编程与内核之网络通信

韩乔落

概述

在互联网时代,网络通信编程已经是一个程序员必不可少的技能之一。几乎所有的产品都会涉及网络操作或访问。在Linux编程环境中,系统提供了socket套接字为程序员提供统一的网络编程接口。这里假设读者有一定的Linux网络编程基础,所以对于系统调用的解释都是点到为止,只针对不常见或容易忽视的问题进行详细说明。后面手写一个tcp/ip协议栈,这里除了基本的API不做过多介绍了。

网路连接的建立

socket文件描述符

socket翻译成中文是插座、插槽的意思,而在网络编程中,其被翻译为“套接字”。Linux环境下,我们经常说“一切皆文件”。因此套接字也被视为一种文件描述符。首先,来看看如何使用socket系统调用创建一个套接字,代码如下:

1
2
3
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

其中的参数解释如下。

  • domain:用于指示协议族名字,如AF_INET为IPv4。
  • type:用于指示类型,如基于流通信的SOCK_STREAM。
  • protocol:用于指示对于这种socket的具体协议类型。一般情况下,使用前两个参数限定后,只会存在一种协议类型对应该情况。这时,可以将protocol设置为0。但是在某些情况下,会存在多个协议类型,这时就必须指定具体的协议类型。
  • 成功创建socket后,会返回一个文件描述符。失败时,该接口返回-1。

那么对于Linux内核来说,如何知道一个文件描述符是一个套接字,还是一个普通文件呢?其实这个问题也可以扩展到,内核如何知道一个文件描述符的具体类型,如何调用实际类型的操作函数呢?这仍然是VFS的魔力。

文件描述符fd与内核文件结构struct file之间的关系,后者是内核用于管理文件的真正结构,其中的成员变量file->f_op为VFS支持的所有文件操作。VFS层无须关心该文件file的实际类型,它会直接调用file->f_op中的操作函数(这样的处理,与面向对象语言中的多态是类似的)。

对于套接字来说,只要在创建套接字时,将file->f_op设置为正确的套接字操作函数即可。该操作是在socket->sock_map_fd->sock_alloc_file中完成的,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int sock_alloc_file(struct socket *sock, struct file **f, int flags)
{
…………
/* 申请一个struct file,并将socket_file_ops作为参数来传递。
* 在alloc_file中,会将socket_file_ops赋给file->f_op。
*/
file = alloc_file(&path, FMODE_READ | FMODE_WRITE, &socket_file_ops);
…………
/* 让sock->file指向file,完成sock和file的关联 */
sock->file = file;
file->f_flags = O_RDWR | (flags & O_NONBLOCK);
file->f_pos = 0;
file->private_data = sock;
*f = file;
return fd;
}

尽管Linux内核是使用C语言编写的,但是其应用了很多面向对象的设计思想。以这里的file为例,内核利用f_op(对象操作函数指针集合)指向具体对象的操作函数集合。这样一来,对于VFS来说,就只须关心struct file,而无须关心具体的对象类型了,它会在处理过程中,调用正确的处理函数。

绑定IP地址

在成功创建套接字后,该套接字仅仅是一个文件描述符,并没有任何地址与之关联。使用该socket发送数据包时,由于该socket没有任何IP地址,内核会根据策略自动选择一个地址。但是,在某些情况下,我们需要手工指定socket使用哪个IP地址进行发送。这时,就需要使用bind系统调用了。

bind的使用

bind系统调用的接口定义如下:

1
2
3
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

其中的参数解释如下。

  • sockfd:表示要绑定地址的套接字描述符。
  • addr:表示绑定到套接字的地址。
  • addrlen:表示绑定的地址长度。
  • 返回值0表示成功,-1则表示错误。

因为Linux的套接字是针对多种协议族的,而每个协议族都可以有不同的地址类型。所以Linux套接字关于地址的系统调用,统一使用了一个公共结构体,并要求调用者将实际地址参数进行强制类型转换,以此来避免编译警告。

1
2
3
4
struct sockaddr {    
sa_family_t sa_family;
char sa_data[14];
}

因为每个协议族的地址类型各不相同,所以需要通过参数addrlen来告诉内核这个地址的实际大小。

struct sockaddr数据类型会在socket涉及地址的所有接口中出现。这是因为套接字接口要支持所有的协议族,所以涉及地址的地方都使用了一个统一的地址结构struct sockaddr。

下面是一个简单示例:

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

#define LOOPBACK_ADDR 0x7F000001
#define LISTEN_PORT 1234

int main(void) {
int sock;
struct sockaddr_in addr;

sock = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sock) {
printf("Fail to create socket\n");
goto err1;
}

addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(LOOPBACK_ADDR);
addr.sin_port = htons(LISTEN_PORT);

if (0 != bind(sock, (struct sockaddr *)&addr, sizeof(addr))) {
printf("Fail to bind\n");
goto err2;
}

if (0 != listen(sock, 3)) {
printf("Fail to listen\n");
goto err2;
}

while (1) {
sleep(3);
}

close(sock);
return 0;

err2:
close(sock);
err1:
return -1;
}

在上面的示例中,我们创建了一个TCP套接字,并将回环地址127.0.0.1和端口1234绑定到这个套接字上。运行这个程序,然后通过netstat检查监听端口:

1
2
3
4
[fgao@ubuntu ~]# netstat -ant
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 127.0.0.1:1234 0.0.0.0:* LISTEN

bind 源码分析

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
SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
// 直接调用内核内部实现的 __sys_bind 函数
return __sys_bind(fd, umyaddr, addrlen);
}

int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
{
struct socket *sock;
struct sockaddr_storage address;
int err, fput_needed;

// 通过文件描述符 fd 查找对应的 socket 结构
// sockfd_lookup_light 是一个轻量级的查找函数
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
// 将用户空间的地址 umyaddr 复制到内核空间的 address 变量中
err = move_addr_to_kernel(umyaddr, addrlen, &address);
if (!err) {
// 执行安全性检查,确保当前进程有权限执行 bind 操作
err = security_socket_bind(sock,
(struct sockaddr *)&address,
addrlen);
if (!err)
// 调用具体协议的 bind 实现
// 通过 sock->ops->bind 函数指针调用协议相关的 bind 操作
err = sock->ops->bind(sock,
(struct sockaddr *)&address, addrlen);
}
// 释放对 socket 文件结构的引用
fput_light(sock->file, fput_needed);
}
// 返回操作的结果,成功返回 0,失败返回负的错误代码
return err;
}

在bind的调用中,根据不同的协议调用不同的实现函数(Linux的内核代码中,大量使用了这种面向对象的设计思路)。对于AF_INET协议族来说,无论是面向连接的SOCK_STREAM类型,还是SOCK_DGRAM协议类型,其实现函数均是inet_bind。下面来看一下inet_bind的具体实现:

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
int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
struct sock *sk = sock->sk; // 获取与 socket 关联的 sock 结构
int err;

// 如果套接字协议有自己的绑定函数,则直接调用它(例如 RAW 套接字)
if (sk->sk_prot->bind) {
return sk->sk_prot->bind(sk, uaddr, addr_len);
}

// 检查地址长度是否小于 sockaddr_in 结构的大小
if (addr_len < sizeof(struct sockaddr_in))
return -EINVAL; // 返回无效参数错误

/* 在进行任何检查之前运行 BPF 程序,以便如果程序以错误的方式更改上下文,
* 它将被捕获。
*/
err = BPF_CGROUP_RUN_PROG_INET4_BIND(sk, uaddr);
if (err)
return err; // 如果 BPF 程序返回错误,则直接返回

// 调用内部的 __inet_bind 函数执行实际的绑定操作
return __inet_bind(sk, uaddr, addr_len, false, true);
}
EXPORT_SYMBOL(inet_bind); // 导出符号以供其他内核模块使用
int __inet_bind(struct sock *sk, struct sockaddr *uaddr, int addr_len,
bool force_bind_address_no_port, bool with_lock)
{
struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;
struct inet_sock *inet = inet_sk(sk);
struct net *net = sock_net(sk);
unsigned short snum;
int chk_addr_ret;
u32 tb_id = RT_TABLE_LOCAL;
int err;

if (addr->sin_family != AF_INET) {
/* Compatibility games : accept AF_UNSPEC (mapped to AF_INET)
* only if s_addr is INADDR_ANY.
*/
err = -EAFNOSUPPORT;
if (addr->sin_family != AF_UNSPEC ||
addr->sin_addr.s_addr != htonl(INADDR_ANY))
goto out;
}

tb_id = l3mdev_fib_table_by_index(net, sk->sk_bound_dev_if) ? : tb_id;
chk_addr_ret = inet_addr_type_table(net, addr->sin_addr.s_addr, tb_id);

/* Not specified by any standard per-se, however it breaks too
* many applications when removed. It is unfortunate since
* allowing applications to make a non-local bind solves
* several problems with systems using dynamic addressing.
* (ie. your servers still start up even if your ISDN link
* is temporarily down)
*/
err = -EADDRNOTAVAIL;
if (!inet_can_nonlocal_bind(net, inet) &&
addr->sin_addr.s_addr != htonl(INADDR_ANY) &&
chk_addr_ret != RTN_LOCAL &&
chk_addr_ret != RTN_MULTICAST &&
chk_addr_ret != RTN_BROADCAST)
goto out;

snum = ntohs(addr->sin_port);
err = -EACCES;
if (snum && inet_port_requires_bind_service(net, snum) &&
!ns_capable(net->user_ns, CAP_NET_BIND_SERVICE))
goto out;

/* We keep a pair of addresses. rcv_saddr is the one
* used by hash lookups, and saddr is used for transmit.
*
* In the BSD API these are the same except where it
* would be illegal to use them (multicast/broadcast) in
* which case the sending device address is used.
*/
if (with_lock)
lock_sock(sk);

/* Check these errors (active socket, double bind). */
err = -EINVAL;
if (sk->sk_state != TCP_CLOSE || inet->inet_num)
goto out_release_sock;

inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;
if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)
inet->inet_saddr = 0; /* Use device */

/* Make sure we are allowed to bind here. */
if (snum || !(inet->bind_address_no_port ||
force_bind_address_no_port)) {
if (sk->sk_prot->get_port(sk, snum)) {
inet->inet_saddr = inet->inet_rcv_saddr = 0;
err = -EADDRINUSE;
goto out_release_sock;
}
err = BPF_CGROUP_RUN_PROG_INET4_POST_BIND(sk);
if (err) {
inet->inet_saddr = inet->inet_rcv_saddr = 0;
goto out_release_sock;
}
}

if (inet->inet_rcv_saddr)
sk->sk_userlocks |= SOCK_BINDADDR_LOCK;
if (snum)
sk->sk_userlocks |= SOCK_BINDPORT_LOCK;
inet->inet_sport = htons(inet->inet_num);
inet->inet_daddr = 0;
inet->inet_dport = 0;
sk_dst_reset(sk);
err = 0;
out_release_sock:
if (with_lock)
release_sock(sk);
out:
return err;
}

无论是APUE还是man手册,在讲解bind的时候都有点问题,或有偏差,或不够详尽。从上面的源码我们知道,通过使用系统控制开关sysctl_ip_nonlocal_bind或套接字选项可以让套接字bind一个非本机地址。但APUE却说套接字只能绑定本机的有效地址——当然这也是由于APUE距现在的时间太久了,而man手册都没有提及非本机地址的事情。

客户端连接过程

connect 的使用

1
2
3
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

其中的参数解释如下:

  • int sockfd:套接字描述符。
  • const struct sockaddr *addr:要连接的地址。
  • socklen_t addrlen:要连接的地址长度。
  • 返回值0表示成功,-1表示失败。

connect的用途是使用指定的套接字去连接指定的地址。对于面向连接的协议(套接字类型为SOCK_STREAM),connect只能成功一次(当然要如此,因为真正的连接已经建立了)。如果重复调用connect,会返回-1表示失败,同时错误码为EISCONN。而对于非面向连接的协议(套接字类型为SOCK_DGRAM),则可以执行多次connect(因为这时的connect仅仅是设置了默认的目的地址)。

对于TCP套接字来说,connect实际上是要真正地进行三次握手,所以其默认是一个阻塞操作。那么是否可以写一个非阻塞的TCP connect代码呢?这是一个合格的网络开发工程师的基本功,具体的实现可以参看UNPv1的实现。更重要是要理解其原理,这样才能在需要的时候,信手拈来。

connect 源码分析

connect的源码入口位于socket.c,代码如下:

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
SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
int, addrlen)
{
return __sys_connect(fd, uservaddr, addrlen);
}
int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)
{
int ret = -EBADF; // 初始化返回值为 -EBADF,表示文件描述符无效
struct fd f;

// 获取文件描述符 fd 对应的文件结构
f = fdget(fd);
if (f.file) { // 如果文件结构存在
struct sockaddr_storage address;

// 将用户空间的地址 uservaddr 复制到内核空间的 address 变量中
ret = move_addr_to_kernel(uservaddr, addrlen, &address);
if (!ret) // 如果地址复制成功
// 调用内部函数 __sys_connect_file 执行连接操作
ret = __sys_connect_file(f.file, &address, addrlen, 0);

// 如果 f.flags 不为 0,释放文件结构的引用
if (f.flags)
fput(f.file);
}

return ret; // 返回操作的结果
}
int __sys_connect_file(struct file *file, struct sockaddr_storage *address,
int addrlen, int file_flags)
{
struct socket *sock;
int err;

// 从文件结构中获取 socket 结构
sock = sock_from_file(file, &err);
if (!sock) // 如果获取失败
goto out; // 跳转到 out 标签,返回错误码

// 执行安全性检查,确保连接操作被允许
err = security_socket_connect(sock, (struct sockaddr *)address, addrlen);
if (err) // 如果安全检查失败
goto out; // 跳转到 out 标签,返回错误码

// 调用 socket 操作的 connect 方法执行连接操作
err = sock->ops->connect(sock, (struct sockaddr *)address, addrlen,
sock->file->f_flags | file_flags);
out:
return err; // 返回操作的结果
}

对于AF_INET协议族来说,面向连接的协议类型是SOCK_STREAM,其连接函数为inet_stream_connect,而非面向连接的协议类型SOCK_DGRAM,其连接函数为inet_dgram_connect。这很合理,因为从connect的功能实现上看,两者的实现效果完全不同。让我们先从简单的inet_dgram_connect入手。

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
int inet_dgram_connect(struct socket *sock, struct sockaddr *uaddr,
int addr_len, int flags)
{
struct sock *sk = sock->sk;
int err;

if (addr_len < sizeof(uaddr->sa_family))
return -EINVAL;
if (uaddr->sa_family == AF_UNSPEC)
return sk->sk_prot->disconnect(sk, flags);

if (BPF_CGROUP_PRE_CONNECT_ENABLED(sk)) {
err = sk->sk_prot->pre_connect(sk, uaddr, addr_len);
if (err)
return err;
}

if (!inet_sk(sk)->inet_num && inet_autobind(sk))
return -EAGAIN;
return sk->sk_prot->connect(sk, uaddr, addr_len);
}
EXPORT_SYMBOL(inet_dgram_connect);
int ip4_datagram_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
int res;

lock_sock(sk);
res = __ip4_datagram_connect(sk, uaddr, addr_len);
release_sock(sk);
return res;
}
EXPORT_SYMBOL(ip4_datagram_connect);
int __ip4_datagram_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
struct inet_sock *inet = inet_sk(sk);
struct sockaddr_in *usin = (struct sockaddr_in *) uaddr;
struct flowi4 *fl4;
struct rtable *rt;
__be32 saddr;
int oif;
int err;

// 检查地址长度是否足够
if (addr_len < sizeof(*usin))
return -EINVAL;

// 检查地址族是否为 AF_INET
if (usin->sin_family != AF_INET)
return -EAFNOSUPPORT;

// 重置套接字的路由缓存
sk_dst_reset(sk);

// 获取绑定的设备接口索引和源地址
oif = sk->sk_bound_dev_if;
saddr = inet->inet_saddr;

// 处理多播地址的情况
if (ipv4_is_multicast(usin->sin_addr.s_addr)) {
if (!oif || netif_index_is_l3_master(sock_net(sk), oif))
oif = inet->mc_index;
if (!saddr)
saddr = inet->mc_addr;
}

// 设置流信息
fl4 = &inet->cork.fl.u.ip4;

// 查找路由
rt = ip_route_connect(fl4, usin->sin_addr.s_addr, saddr,
RT_CONN_FLAGS(sk), oif,
sk->sk_protocol,
inet->inet_sport, usin->sin_port, sk);
if (IS_ERR(rt)) {
err = PTR_ERR(rt);
if (err == -ENETUNREACH)
IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);
goto out;
}

// 检查是否为广播地址且套接字不允许广播
if ((rt->rt_flags & RTCF_BROADCAST) && !sock_flag(sk, SOCK_BROADCAST)) {
ip_rt_put(rt);
err = -EACCES;
goto out;
}

// 更新源地址
if (!inet->inet_saddr)
inet->inet_saddr = fl4->saddr;

// 更新接收源地址
if (!inet->inet_rcv_saddr) {
inet->inet_rcv_saddr = fl4->saddr;
if (sk->sk_prot->rehash)
sk->sk_prot->rehash(sk);
}

// 设置目标地址和端口
inet->inet_daddr = fl4->daddr;
inet->inet_dport = usin->sin_port;

// 更新重用端口状态
reuseport_has_conns(sk, true);

// 设置套接字状态为已连接
sk->sk_state = TCP_ESTABLISHED;

// 设置传输哈希
sk_set_txhash(sk);

// 随机生成 IP 标识符
inet->inet_id = prandom_u32();

// 设置套接字的路由缓存
sk_dst_set(sk, &rt->dst);

err = 0;
out:
return err;
}
EXPORT_SYMBOL(__ip4_datagram_connect);

由于功能比较简单,所以UDP的connect实现源码也一目了然,可以看到,只是设置了目的IP、端口和路由信息。


下面看一下TCP的connect实现,代码如下:

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
int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
int addr_len, int flags)
{
int err;

lock_sock(sock->sk);
err = __inet_stream_connect(sock, uaddr, addr_len, flags, 0);
release_sock(sock->sk);
return err;
}
int __inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
int addr_len, int flags, int is_sendmsg)
{
struct sock *sk = sock->sk;
int err;
long timeo;

// 检查地址参数的有效性
if (uaddr) {
if (addr_len < sizeof(uaddr->sa_family))
return -EINVAL;

// 如果地址族是 AF_UNSPEC,断开连接
if (uaddr->sa_family == AF_UNSPEC) {
err = sk->sk_prot->disconnect(sk, flags);
sock->state = err ? SS_DISCONNECTING : SS_UNCONNECTED;
goto out;
}
}

// 根据套接字的当前状态进行处理
switch (sock->state) {
default:
err = -EINVAL;
goto out;
case SS_CONNECTED:
err = -EISCONN;
goto out;
case SS_CONNECTING:
if (inet_sk(sk)->defer_connect)
err = is_sendmsg ? -EINPROGRESS : -EISCONN;
else
err = -EALREADY;
break;
case SS_UNCONNECTED:
err = -EISCONN;
if (sk->sk_state != TCP_CLOSE)
goto out;

// 如果启用了 BPF 预连接钩子,调用预连接处理函数
if (BPF_CGROUP_PRE_CONNECT_ENABLED(sk)) {
err = sk->sk_prot->pre_connect(sk, uaddr, addr_len);
if (err)
goto out;
}

// 尝试连接
err = sk->sk_prot->connect(sk, uaddr, addr_len);
if (err < 0)
goto out;

sock->state = SS_CONNECTING;

if (!err && inet_sk(sk)->defer_connect)
goto out;

err = -EINPROGRESS;
break;
}

// 获取发送超时时间
timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);

// 如果套接字处于 SYN_SENT 或 SYN_RECV 状态,等待连接完成
if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
int writebias = (sk->sk_protocol == IPPROTO_TCP) &&
tcp_sk(sk)->fastopen_req &&
tcp_sk(sk)->fastopen_req->data ? 1 : 0;

if (!timeo || !inet_wait_for_connect(sk, timeo, writebias))
goto out;

err = sock_intr_errno(timeo);
if (signal_pending(current))
goto out;
}

// 检查连接是否被关闭
if (sk->sk_state == TCP_CLOSE)
goto sock_error;

// 设置套接字状态为已连接
sock->state = SS_CONNECTED;
err = 0;
out:
return err;

sock_error:
err = sock_error(sk) ? : -ECONNABORTED;
sock->state = SS_UNCONNECTED;
if (sk->sk_prot->disconnect(sk, flags))
sock->state = SS_DISCONNECTING;
goto out;
}

接下来,就需要进入TCP协议自定义的connect函数tcp_v4_connect了,代码如下:

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
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
struct inet_sock *inet = inet_sk(sk);
struct tcp_sock *tp = tcp_sk(sk);
__be16 orig_sport, orig_dport;
__be32 daddr, nexthop;
struct flowi4 *fl4;
struct rtable *rt;
int err;
struct ip_options_rcu *inet_opt;
struct inet_timewait_death_row *tcp_death_row = &sock_net(sk)->ipv4.tcp_death_row;

// 检查地址长度和地址族
if (addr_len < sizeof(struct sockaddr_in))
return -EINVAL;

if (usin->sin_family != AF_INET)
return -EAFNOSUPPORT;

// 获取目的地址和下一跳地址
nexthop = daddr = usin->sin_addr.s_addr;
inet_opt = rcu_dereference_protected(inet->inet_opt, lockdep_sock_is_held(sk));
if (inet_opt && inet_opt->opt.srr) {
if (!daddr)
return -EINVAL;
nexthop = inet_opt->opt.faddr;
}

// 保存原始的源端口和目的端口
orig_sport = inet->inet_sport;
orig_dport = usin->sin_port;
fl4 = &inet->cork.fl.u.ip4;

// 查找路由
rt = ip_route_connect(fl4, nexthop, inet->inet_saddr, RT_CONN_FLAGS(sk), sk->sk_bound_dev_if, IPPROTO_TCP, orig_sport, orig_dport, sk);
if (IS_ERR(rt)) {
err = PTR_ERR(rt);
if (err == -ENETUNREACH)
IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);
return err;
}

// 检查路由标志
if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) {
ip_rt_put(rt);
return -ENETUNREACH;
}

// 设置目的地址
if (!inet_opt || !inet_opt->opt.srr)
daddr = fl4->daddr;

// 设置源地址
if (!inet->inet_saddr)
inet->inet_saddr = fl4->saddr;
sk_rcv_saddr_set(sk, inet->inet_saddr);

// 重置 TCP 时间戳选项
if (tp->rx_opt.ts_recent_stamp && inet->inet_daddr != daddr) {
tp->rx_opt.ts_recent = 0;
tp->rx_opt.ts_recent_stamp = 0;
if (likely(!tp->repair))
WRITE_ONCE(tp->write_seq, 0);
}

// 设置目的端口和地址
inet->inet_dport = usin->sin_port;
sk_daddr_set(sk, daddr);

// 设置扩展头长度
inet_csk(sk)->icsk_ext_hdr_len = 0;
if (inet_opt)
inet_csk(sk)->icsk_ext_hdr_len = inet_opt->opt.optlen;

// 设置 MSS 限制
tp->rx_opt.mss_clamp = TCP_MSS_DEFAULT;

// 设置套接字状态为 SYN_SENT
tcp_set_state(sk, TCP_SYN_SENT);
err = inet_hash_connect(tcp_death_row, sk);
if (err)
goto failure;

sk_set_txhash(sk);

// 更新路由端口
rt = ip_route_newports(fl4, rt, orig_sport, orig_dport, inet->inet_sport, inet->inet_dport, sk);
if (IS_ERR(rt)) {
err = PTR_ERR(rt);
rt = NULL;
goto failure;
}

// 设置套接字的 GSO 类型和能力
sk->sk_gso_type = SKB_GSO_TCPV4;
sk_setup_caps(sk, &rt->dst);
rt = NULL;

// 设置 TCP 序列号和时间戳偏移
if (likely(!tp->repair)) {
if (!tp->write_seq)
WRITE_ONCE(tp->write_seq, secure_tcp_seq(inet->inet_saddr, inet->inet_daddr, inet->inet_sport, usin->sin_port));
tp->tsoffset = secure_tcp_ts_off(sock_net(sk), inet->inet_saddr, inet->inet_daddr);
}

// 设置 IP 标识符
inet->inet_id = prandom_u32();

// 处理 TCP 快速打开
if (tcp_fastopen_defer_connect(sk, &err))
return err;
if (err)
goto failure;

// 发起 TCP 连接
err = tcp_connect(sk);
if (err)
goto failure;

return 0;

failure:
// 处理失败情况,重置套接字状态
tcp_set_state(sk, TCP_CLOSE);
ip_rt_put(rt);
sk->sk_route_caps = 0;
inet->inet_dport = 0;
return err;
}

下面来分析tcp_connect,看看内核是如何发送SYN包的,代码如下:

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
int tcp_connect(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *buff;
int err;

// 调用 BPF 钩子函数,用于 TCP 连接的回调
tcp_call_bpf(sk, BPF_SOCK_OPS_TCP_CONNECT_CB, 0, NULL);

// 重建 IP 头部,如果失败则返回错误
if (inet_csk(sk)->icsk_af_ops->rebuild_header(sk))
return -EHOSTUNREACH; // 路由失败或类似问题

// 初始化 TCP 连接参数
tcp_connect_init(sk);

// 如果处于 TCP 修复模式,直接完成连接
if (unlikely(tp->repair)) {
tcp_finish_connect(sk, NULL);
return 0;
}

// 分配一个新的 sk_buff 用于发送 SYN 包
buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true);
if (unlikely(!buff))
return -ENOBUFS; // 缓冲区不足

// 初始化非数据包的 SKB,设置 SYN 标志
tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);
tcp_mstamp_refresh(tp);
tp->retrans_stamp = tcp_time_stamp(tp);

// 将 SKB 加入发送队列
tcp_connect_queue_skb(sk, buff);
tcp_ecn_send_syn(sk, buff);
tcp_rbtree_insert(&sk->tcp_rtx_queue, buff);

// 发送 SYN 包,如果启用了快速打开则包括数据
err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :
tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
if (err == -ECONNREFUSED)
return err;

// 更新发送序列号
WRITE_ONCE(tp->snd_nxt, tp->write_seq);
tp->pushed_seq = tp->write_seq;
buff = tcp_send_head(sk);
if (unlikely(buff)) {
WRITE_ONCE(tp->snd_nxt, TCP_SKB_CB(buff)->seq);
tp->pushed_seq = TCP_SKB_CB(buff)->seq;
}

// 增加活动打开连接的统计计数
TCP_INC_STATS(sock_net(sk), TCP_MIB_ACTIVEOPENS);

// 设置重传定时器以重复发送 SYN 直到收到响应
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
return 0;
}

服务端连接过程

listen 的使用

服务器端用listen来监听端口,其原型为:

1
2
3
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
  • 参数int sockfd:成功创建的TCP套接字。
  • int backlog:定义TCP未处理连接的队列长度。该队列虽然已经完成了三次握手,但服务器端还没有执行accept的连接。
  • 函数的返回值为0,表示成功;-1表示失败。

listen 源码分析

listen的源码入口位于socket.c,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
return __sys_listen(fd, backlog);
}
int __sys_listen(int fd, int backlog)
{
struct socket *sock;
int err, fput_needed;
int somaxconn;

sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
if ((unsigned int)backlog > somaxconn)
backlog = somaxconn;

err = security_socket_listen(sock, backlog);
if (!err)
err = sock->ops->listen(sock, backlog);

fput_light(sock->file, fput_needed);
}
return err;
}

AF_INET协议族的listen实现函数为inet_listen,代码如下:

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
int inet_listen(struct socket *sock, int backlog)
{
struct sock *sk = sock->sk;
unsigned char old_state;
int err, tcp_fastopen;

// 获取套接字的锁,以确保对套接字状态的修改是线程安全的
lock_sock(sk);

// 初始化错误码为无效参数
err = -EINVAL;
// 检查套接字状态是否为未连接,且类型是否为流式套接字
if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
goto out;

// 获取当前套接字的状态
old_state = sk->sk_state;
// 检查套接字状态是否为关闭或监听状态
if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
goto out;

// 设置最大确认的积压队列长度
WRITE_ONCE(sk->sk_max_ack_backlog, backlog);

// 如果套接字已经处于监听状态,只允许调整积压队列长度
if (old_state != TCP_LISTEN) {
// 启用 TCP 快速打开(TFO),无需显式设置 TCP_FASTOPEN 套接字选项
tcp_fastopen = sock_net(sk)->ipv4.sysctl_tcp_fastopen;
if ((tcp_fastopen & TFO_SERVER_WO_SOCKOPT1) &&
(tcp_fastopen & TFO_SERVER_ENABLE) &&
!inet_csk(sk)->icsk_accept_queue.fastopenq.max_qlen) {
fastopen_queue_tune(sk, backlog);
tcp_fastopen_init_key_once(sock_net(sk));
}

// 启动监听
err = inet_csk_listen_start(sk, backlog);
if (err)
goto out;

// 调用 BPF 钩子函数,用于 TCP 监听的回调
tcp_call_bpf(sk, BPF_SOCK_OPS_TCP_LISTEN_CB, 0, NULL);
}
// 成功设置监听状态
err = 0;

out:
// 释放套接字的锁
release_sock(sk);
return err;
}

接下来进入inet_csk_listen_start,代码如下:

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
int inet_csk_listen_start(struct sock *sk, int backlog)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct inet_sock *inet = inet_sk(sk);
int err = -EADDRINUSE;

// 为接受队列分配资源
reqsk_queue_alloc(&icsk->icsk_accept_queue);

// 初始化确认积压队列长度和延迟确认
sk->sk_ack_backlog = 0;
inet_csk_delack_init(sk);

/* 这里存在一个竞争窗口:我们宣布自己正在监听,
* 但此转换尚未通过 get_port() 验证。
* 这是可以的,因为只有在验证完成后,该套接字才会进入哈希表。
*/
inet_sk_state_store(sk, TCP_LISTEN);

// 尝试获取端口号
if (!sk->sk_prot->get_port(sk, inet->inet_num)) {
// 设置套接字的源端口
inet->inet_sport = htons(inet->inet_num);

// 重置套接字的路由缓存
sk_dst_reset(sk);

// 将套接字插入到协议的哈希表中
err = sk->sk_prot->hash(sk);

// 如果成功插入哈希表,返回 0 表示成功
if (likely(!err))
return 0;
}

// 如果失败,将套接字状态设置为关闭
inet_sk_set_state(sk, TCP_CLOSE);
return err;
}

现在服务器端已经处于监听状态,可以接收客户端的连接请求了。同时,通过源码跟踪,也可以发现在第二个参数不超过系统限制的最大值的情况下,内核已直接使用其值作为已连接队列的长度了。

accept 的使用

accept用于从指定套接字的连接队列中取出第一个连接,并返回一个新的套接字用于与客户端进行通信,示例代码如下:

1
2
3
4
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);

其中的参数解释如下:

  • int sockfd:处于监听状态的套接字。
  • struct sockaddr *addr:用于保存对端的地址信息。
  • socklen_t *addrlen:是一个输入输出值。调用者将其初始化为addr缓存的大小,accept返回时,会将其设置为addr的大小。
  • int flags:是新引入的系统调用accept4的标志位;目前支持SOCK_NONBLOCKSOCK_CLOEXEC。关于返回值,若执行成功,则返回一个非负的文件描述符;若失败则返回-1。

若不关心对端地址信息,则可以将addr和addrlen设置为NULL。

accept 源码分析

accept的源码入口位于文件socket.c,代码如下:

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
SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr,
int __user *, upeer_addrlen)
{
return __sys_accept4(fd, upeer_sockaddr, upeer_addrlen, 0);
}
SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr,
int __user *, upeer_addrlen, int, flags)
{
return __sys_accept4(fd, upeer_sockaddr, upeer_addrlen, flags);
}
int __sys_accept4(int fd, struct sockaddr __user *upeer_sockaddr,
int __user *upeer_addrlen, int flags)
{
int ret = -EBADF;
struct fd f;

f = fdget(fd);
if (f.file) {
ret = __sys_accept4_file(f.file, 0, upeer_sockaddr,
upeer_addrlen, flags,
rlimit(RLIMIT_NOFILE));
if (f.flags)
fput(f.file);
}

return ret;
}
int __sys_accept4_file(struct file *file, unsigned file_flags,
struct sockaddr __user *upeer_sockaddr,
int __user *upeer_addrlen, int flags,
unsigned long nofile)
{
struct socket *sock, *newsock;
struct file *newfile;
int err, len, newfd;
struct sockaddr_storage address;

// 检查 flags 是否只包含 SOCK_CLOEXEC 和 SOCK_NONBLOCK
if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
return -EINVAL;

// 如果 SOCK_NONBLOCK 不等于 O_NONBLOCK 并且 flags 包含 SOCK_NONBLOCK,则替换为 O_NONBLOCK
if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;

// 从文件结构中获取 socket
sock = sock_from_file(file, &err);
if (!sock)
goto out;

// 分配新的 socket 结构
err = -ENFILE;
newsock = sock_alloc();
if (!newsock)
goto out;

// 复制套接字类型和操作
newsock->type = sock->type;
newsock->ops = sock->ops;

/*
* 不需要在这里调用 try_module_get,因为监听套接字 (sock)
* 已经持有协议模块 (sock->ops->owner)。
*/
__module_get(newsock->ops->owner);

// 获取一个未使用的文件描述符
newfd = __get_unused_fd_flags(flags, nofile);
if (unlikely(newfd < 0)) {
err = newfd;
sock_release(newsock);
goto out;
}

// 为新 socket 分配文件结构
newfile = sock_alloc_file(newsock, flags, sock->sk->sk_prot_creator->name);
if (IS_ERR(newfile)) {
err = PTR_ERR(newfile);
put_unused_fd(newfd);
goto out;
}

// 安全检查
err = security_socket_accept(sock, newsock);
if (err)
goto out_fd;

// 调用协议的 accept 方法
err = sock->ops->accept(sock, newsock, sock->file->f_flags | file_flags, false);
if (err < 0)
goto out_fd;

// 如果用户提供了对等地址缓冲区,则获取对等地址
if (upeer_sockaddr) {
len = newsock->ops->getname(newsock, (struct sockaddr *)&address, 2);
if (len < 0) {
err = -ECONNABORTED;
goto out_fd;
}
err = move_addr_to_user(&address, len, upeer_sockaddr, upeer_addrlen);
if (err < 0)
goto out_fd;
}

/* 文件标志不会通过 accept() 继承,和其他操作系统不同。 */

// 安装新的文件描述符
fd_install(newfd, newfile);
err = newfd;
out:
return err;
out_fd:
fput(newfile);
put_unused_fd(newfd);
goto out;
}

对于AF_INET协议族,accept的实现函数为inet_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
int inet_accept(struct socket *sock, struct socket *newsock, int flags,
bool kern)
{
struct sock *sk1 = sock->sk;
int err = -EINVAL;
struct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err, kern);

if (!sk2)
goto do_err;

lock_sock(sk2);

sock_rps_record_flow(sk2);
WARN_ON(!((1 << sk2->sk_state) &
(TCPF_ESTABLISHED | TCPF_SYN_RECV |
TCPF_CLOSE_WAIT | TCPF_CLOSE)));

sock_graft(sk2, newsock);

newsock->state = SS_CONNECTED;
err = 0;
release_sock(sk2);
do_err:
return err;
}

对于TCP协议来说,其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
struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct request_sock_queue *queue = &icsk->icsk_accept_queue;
struct request_sock *req;
struct sock *newsk;
int error;

lock_sock(sk);

/* 确保套接字处于监听状态,并且有挂起的连接请求 */
error = -EINVAL;
if (sk->sk_state != TCP_LISTEN)
goto out_err;

/* 查找已建立的连接 */
if (reqsk_queue_empty(queue)) {
long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);

/* 如果是非阻塞套接字,则不等待 */
error = -EAGAIN;
if (!timeo)
goto out_err;

error = inet_csk_wait_for_connect(sk, timeo);
if (error)
goto out_err;
}

// 从请求队列中移除一个请求
req = reqsk_queue_remove(queue, sk);
newsk = req->sk;

// 处理 TCP Fast Open 的情况
if (sk->sk_protocol == IPPROTO_TCP &&
tcp_rsk(req)->tfo_listener) {
spin_lock_bh(&queue->fastopenq.lock);
if (tcp_rsk(req)->tfo_listener) {
/* 我们仍在等待 3WHS 的最终 ACK,因此现在不能释放 req。
* 相反,我们将 req->sk 设置为 NULL,以表示子套接字已被获取,
* 因此当 3WHS 完成(或中止)时,reqsk_fastopen_remove() 将释放 req。
*/
req->sk = NULL;
req = NULL;
}
spin_unlock_bh(&queue->fastopenq.lock);
}

out:
release_sock(sk);
if (newsk && mem_cgroup_sockets_enabled) {
int amt;

/* 原子地获取内存使用情况,设置并收费 newsk->sk_memcg。 */
lock_sock(newsk);

/* 套接字尚未被接受,因此无需查看 newsk->sk_wmem_queued。 */
amt = sk_mem_pages(newsk->sk_forward_alloc +
atomic_read(&newsk->sk_rmem_alloc));
mem_cgroup_sk_alloc(newsk);
if (newsk->sk_memcg && amt)
mem_cgroup_charge_skmem(newsk->sk_memcg, amt);

release_sock(newsk);
}
if (req)
reqsk_put(req);
return newsk;

out_err:
newsk = NULL;
req = NULL;
*err = error;
goto out;
}

TCP 三次握手的实现分析

前面从客户端和服务器端的系统调用的角度,来分析和学习TCP的连接过程。本节将从TCP三次握手的数据包交互过程,来研究TCP连接的建立。。

TCP报文格式:

2645999549-5e99b7b074b37_fix732

TCP 首部包含以下内容,请留意其中的控制位,在三次握手和四次挥手过程中会频繁出现:

  • **端口号 (Source Port and Destination Port)**:每个 TCP 报文段都包含源端和目的端的端口号,用于寻找发送端和接收端应用进程。这两个值加上 IP 首部中的源端 IP 地址和目的端 IP 地址就可以确定一个唯一的 TCP 连接。
  • **序号 (Sequence Number)**:这个字段的主要作用是用于将失序的数据重新排列。TCP 会隐式地对字节流中的每个字节进行编号,而 TCP 报文段的序号被设置为其数据部分的第一个字节的编号。序号是 32 bit 的无符号数,取值范围是0到 232 - 1。
  • **确认序号 (Acknowledgment Number)**:接收方在接受到数据后,会回复确认报文,其中包含确认序号,作用就是告诉发送方自己接收到了哪些数据,下一次数据从哪里开始发,因此,确认序号应当是上次已成功收到数据字节序号加 1。只有 ACK 标志为 1 时确认序号字段才有效。
  • **首部长度 (Header Length)**:首部中的选项部分的长度是可变的,因此首部的长度也是可变的,所以需要这个字段来明确表示首部的长度,这个字段占 4 bit,4 位的二进制数最大可以表示 15,而首部长度是以 4 个字节为一个单位的,因此首部最大长度是 15 * 4 = 60 字节。
  • **保留字段 (Reserved)**:占 6 位,未来可能有具体用途,目前默认值为0.
  • **控制位 (Control Bits)**:在三次握手和四次挥手中会经常看到 SYN、ACK 和 FIN 的身影,一共有 6 个标志位,它们表示的意义如下:
    • **URG (Urgent Bit)**:值为 1 时,紧急指针生效
    • **ACK (Acknowledgment Bit)**:值为 1 时,确认序号生效
    • **PSH (Push Bit)**:接收方应尽快将这个报文段交给应用层
    • **RST (Reset Bit)**:发送端遇到问题,想要重建连接
    • **SYN (Synchronize Bit)**:同步序号,用于发起一个连接
    • **FIN (Finish Bit)**:发送端要求关闭连接
  • **窗口大小 (Window)**: TCP的流量控制由连接的每一端通过声明的窗口大小来提供。窗口大小为字节数,起始于确认序号字段指明的值,这个值是接收端正期望接收的字节。窗口大小是一个 16 bit 字段,单位是字节, 因而窗口大小最大为 65535 字节。
  • **检验和 (Checksum)**:功能类似于数字签名,用于验证数据完整性,也就是确保数据未被修改。检验和覆盖了整个 TCP 报文段,包括 TCP 首部和 TCP 数据,发送端根据特定算法对整个报文段计算出一个检验和,接收端会进行计算并验证。
  • **紧急指针 (Urgent Pointer)**:当 URG 控制位值为 1 时,此字段生效,紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。 TCP 的紧急方式是发送端向另一端发送紧急数据的一种方式。
  • **选项 (Options)**:这一部分是可选字段,也就是非必须字段,最常见的可选字段是“最长报文大小 (MSS,Maximum Segment Size)”。
  • **有效数据部分 (Data)**:这部分也不是必须的,比如在建立和关闭 TCP 连接的阶段,双方交换的报文段就只包含 TCP 首部。

TCP三次握手

3507527407-5e7b1e199cbbb_fix732

三次握手

  1. 第一次握手:客户端向服务器发送报文段1,其中的 SYN 标志位 (前文已经介绍过各种标志位的作用)的值为 1,表示这是一个用于请求发起连接的报文段,其中的序号字段 (Sequence Number,图中简写为seq)被设置为初始序号x (Initial Sequence Number,ISN),TCP 连接双方均可随机选择初始序号。发送完报文段1之后,客户端进入 SYN-SENT 状态,等待服务器的确认。
  2. 第二次握手:服务器在收到客户端的连接请求后,向客户端发送报文段2作为应答,其中 ACK 标志位设置为 1,表示对客户端做出应答,其确认序号字段 (Acknowledgment Number,图中简写为小写 ack) 生效,该字段值为 x + 1,也就是从客户端收到的报文段的序号加一,代表服务器期望下次收到客户端的数据的序号。此外,报文段2的 SYN 标志位也设置为1,代表这同时也是一个用于发起连接的报文段,序号 seq 设置为服务器初始序号y。发送完报文段2后,服务器进入 SYN-RECEIVED 状态。
  3. 第三次握手:客户端在收到报文段2后,向服务器发送报文段3,其 ACK 标志位为1,代表对服务器做出应答,确认序号字段 ack 为 y + 1,序号字段 seq 为 x + 1。此报文段发送完毕后,双方都进入 ESTABLISHED 状态,表示连接已建立。

同时打开

这是 TCP 建立连接的特殊情况,有时会出现两台机器同时执行主动打开的情况,不过概率非常小,这种情况大家仅作了解即可。在这种情况下就无所谓发送方和接收方了,双放都可以称为客户端和服务器,同时打开的过程如下:

3987873264-5e7b1e34b2c51_fix732

如图所示,双方在同一时刻发送 SYN 报文段,并进入 SYN-SENT 状态,在收到 SYN 后,状态变为 SYN-RECEIVED,同时它们都再发送一个 SYN + ACK 的报文段,状态都变为 ESTABLISHED,连接成功建立。在此过程中双方一共交换了4个报文段,比三次握手多一个。

TCP 建立连接为什么要三次握手而不是两次?

答:网上大多数资料对这个问题的回答只有简单的一句:防止已过期的连接请求报文突然又传送到服务器,因而产生错误,这既不够全面也不够具体。下面给出比较详细而全面的回答:

  1. 防止已过期的连接请求报文突然又传送到服务器,因而产生错误

    在双方两次握手即可建立连接的情况下,假设客户端发送 A 报文段请求建立连接,由于网络原因造成 A 暂时无法到达服务器,服务器接收不到请求报文段就不会返回确认报文段,客户端在长时间得不到应答的情况下重新发送请求报文段 B,这次 B 顺利到达服务器,服务器随即返回确认报文并进入 ESTABLISHED 状态,客户端在收到 确认报文后也进入 ESTABLISHED 状态,双方建立连接并传输数据,之后正常断开连接。此时姗姗来迟的 A 报文段才到达服务器,服务器随即返回确认报文并进入 ESTABLISHED 状态,但是已经进入 CLOSED 状态的客户端无法再接受确认报文段,更无法进入 ESTABLISHED 状态,这将导致服务器长时间单方面等待,造成资源浪费。

  2. 三次握手才能让双方均确认自己和对方的发送和接收能力都正常

    第一次握手:客户端只是发送处请求报文段,什么都无法确认,而服务器可以确认自己的接收能力和对方的发送能力正常;

    第二次握手:客户端可以确认自己发送能力和接收能力正常,对方发送能力和接收能力正常;

    第三次握手:服务器可以确认自己发送能力和接收能力正常,对方发送能力和接收能力正常;

    可见三次握手才能让双方都确认自己和对方的发送和接收能力全部正常,这样就可以愉快地进行通信了。

  3. 告知对方自己的初始序号值,并确认收到对方的初始序号值

    TCP 实现了可靠的数据传输,原因之一就是 TCP 报文段中维护了序号字段和确认序号字段,也就是图中的 seq 和 ack,通过这两个字段双方都可以知道在自己发出的数据中,哪些是已经被对方确认接收的。这两个字段的值会在初始序号值得基础递增,如果是两次握手,只有发起方的初始序号可以得到确认,而另一方的初始序号则得不到确认。

TCP 建立连接为什么要三次握手而不是四次?

相比上个问题而言,这个问题就简单多了。因为三次握手已经可以确认双方的发送接收能力正常,双方都知道彼此已经准备好,而且也可以完成对双方初始序号值得确认,也就无需再第四次握手了。

TCP 四次挥手的实现分析

建立一个连接需要三次握手,而终止一个连接要经过 4次握手。这由 TCP 的半关闭( half-close) 造成的。既然一个 TCP 连接是全双工 (即数据在两个方向上能同时传递), 因此每个方向必须单独地进行关闭。这原则就是当一方完成它的数据发送任务后就能发送一个 FIN 来终止这个方向连接。当一端收到一个 FIN,它必须通知应用层另一端已经终止了数据传送。理论上客户端和服务器都可以发起主动关闭,但是更多的情况下是客户端主动发起。

748605951-5e7b1e45d431b_fix732

四次挥手详细过程如下:

  1. 客户端发送关闭连接的报文段,FIN 标志位1,请求关闭连接,并停止发送数据。序号字段 seq = x (等于之前发送的所有数据的最后一个字节的序号加一),然后客户端会进入 FIN-WAIT-1 状态,等待来自服务器的确认报文。
  2. 服务器收到 FIN 报文后,发回确认报文,ACK = 1, ack = x + 1,并带上自己的序号 seq = y,然后服务器就进入 CLOSE-WAIT 状态。服务器还会通知上层的应用程序对方已经释放连接,此时 TCP 处于半关闭状态,也就是说客户端已经没有数据要发送了,但是服务器还可以发送数据,客户端也还能够接收。
  3. 客户端收到服务器的 ACK 报文段后随即进入 FIN-WAIT-2 状态,此时还能收到来自服务器的数据,直到收到 FIN 报文段。
  4. 服务器发送完所有数据后,会向客户端发送 FIN 报文段,各字段值如图所示,随后服务器进入 LAST-ACK 状态,等待来自客户端的确认报文段。
  5. 客户端收到来自服务器的 FIN 报文段后,向服务器发送 ACK 报文,随后进入 TIME-WAIT 状态,等待 2MSL(2 * Maximum Segment Lifetime,两倍的报文段最大存活时间) ,这是任何报文段在被丢弃前能在网络中存在的最长时间,常用值有30秒、1分钟和2分钟。如无特殊情况,客户端会进入 CLOSED 状态。
  6. 服务器在接收到客户端的 ACK 报文后会随即进入 CLOSED 状态,由于没有等待时间,一般而言,服务器比客户端更早进入 CLOSED 状态。

为什么 TCP 关闭连接为什么要四次而不是三次?

服务器在收到客户端的 FIN 报文段后,可能还有一些数据要传输,所以不能马上关闭连接,但是会做出应答,返回 ACK 报文段,接下来可能会继续发送数据,在数据发送完后,服务器会向客户单发送 FIN 报文,表示数据已经发送完毕,请求关闭连接,然后客户端再做出应答,因此一共需要四次挥手。

客户端为什么需要在 TIME-WAIT 状态等待 2MSL 时间才能进入 CLOSED 状态?

按照常理,在网络正常的情况下,四个报文段发送完后,双方就可以关闭连接进入 CLOSED 状态了,但是网络并不总是可靠的,如果客户端发送的 ACK 报文段丢失,服务器在接收不到 ACK 的情况下会一直重发 FIN 报文段,这显然不是我们想要的。因此客户端为了确保服务器收到了 ACK,会设置一个定时器,并在 TIME-WAIT 状态等待 2MSL 的时间,如果在此期间又收到了来自服务器的 FIN 报文段,那么客户端会重新设置计时器并再次等待 2MSL 的时间,如果在这段时间内没有收到来自服务器的 FIN 报文,那就说明服务器已经成功收到了 ACK 报文,此时客户端就可以进入 CLOSED 状态了。

数据报文的发送

发送相关接口

1
2
3
4
5
6
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

send只能用于处理已连接状态的套接字。而sendto可以在调用时,指定目的地址。这样的话,如果套接字已经是连接状态,那么目的地址dest_addr与地址长度就应该为NULL和0,不然就可能会返回错误。sendmsg则比较特殊,无论是要发送的数据还是目的地址,都保存在msg中。其中msg.msg_name和msg.msg_len用于指明目的地址,而msg.msg_iov则用于保存要发送的数据。这三个系统调用都支持设置指示标志位flags。

稍微现代些的系统调用,一般都会拥有或保留一个指示标志参数。通过标志位flags,可以从容地为系统调用增加新功能,并同时兼容老版本。dup、dup2和dup3则是这方面的一个反面典型。在不支持flag的情况下,不得不一再创建新的dup接口,直到dup3加入了对flag的支持为止。

由于socket同时还是文件描述符,所以为文件提供的写操作(如write、writev等),也可以被socket套接字直接调用,在此就不重复叙述了。

数据包从用户空间到内核空间的流程

send的内核实现代码如下:

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
SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
unsigned int, flags, struct sockaddr __user *, addr,
int, addr_len)
{
return __sys_sendto(fd, buff, len, flags, addr, addr_len);
}
SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len,
unsigned int, flags)
{
return __sys_sendto(fd, buff, len, flags, NULL, 0);
}
int __sys_sendto(int fd, void __user *buff, size_t len, unsigned int flags,
struct sockaddr __user *addr, int addr_len)
{
struct socket *sock;
struct sockaddr_storage address;
int err;
struct msghdr msg;
struct iovec iov;
int fput_needed;

// 将用户空间的缓冲区导入到内核空间的 iovec 结构中
err = import_single_range(WRITE, buff, len, &iov, &msg.msg_iter);
if (unlikely(err))
return err;

// 查找文件描述符对应的 socket
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
goto out;

// 初始化消息头结构
msg.msg_name = NULL;
msg.msg_control = NULL;
msg.msg_controllen = 0;
msg.msg_namelen = 0;

// 如果提供了地址,则将地址从用户空间移动到内核空间
if (addr) {
err = move_addr_to_kernel(addr, addr_len, &address);
if (err < 0)
goto out_put;
msg.msg_name = (struct sockaddr *)&address;
msg.msg_namelen = addr_len;
}

// 如果 socket 是非阻塞的,则设置 MSG_DONTWAIT 标志
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;
msg.msg_flags = flags;

// 发送消息
err = sock_sendmsg(sock, &msg);

out_put:
// 释放 socket 引用
fput_light(sock->file, fput_needed);
out:
return err;
}

这里又调用到sock_sendmsg了,从名字上就能感觉到它可能也会被第三个接口sendmsg所调用。下面让我们来验证这个猜想。

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
SYSCALL_DEFINE3(sendmsg, int, fd, struct user_msghdr __user *, msg, unsigned int, flags)
{
return __sys_sendmsg(fd, msg, flags, true);
}
long __sys_sendmsg(int fd, struct user_msghdr __user *msg, unsigned int flags,
bool forbid_cmsg_compat)
{
int fput_needed, err;
struct msghdr msg_sys;
struct socket *sock;

if (forbid_cmsg_compat && (flags & MSG_CMSG_COMPAT))
return -EINVAL;

sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
goto out;

err = ___sys_sendmsg(sock, msg, &msg_sys, flags, NULL, 0);

fput_light(sock->file, fput_needed);
out:
return err;
}
static int ____sys_sendmsg(struct socket *sock, struct msghdr *msg_sys,
unsigned int flags, struct used_address *used_address,
unsigned int allowed_msghdr_flags)
{
unsigned char ctl[sizeof(struct cmsghdr) + 20]
__aligned(sizeof(__kernel_size_t));
/* 20 is size of ipv6_pktinfo */
unsigned char *ctl_buf = ctl;
int ctl_len;
ssize_t err;

err = -ENOBUFS;

if (msg_sys->msg_controllen > INT_MAX)
goto out;
flags |= (msg_sys->msg_flags & allowed_msghdr_flags);
ctl_len = msg_sys->msg_controllen;
if ((MSG_CMSG_COMPAT & flags) && ctl_len) {
err =
cmsghdr_from_user_compat_to_kern(msg_sys, sock->sk, ctl,
sizeof(ctl));
if (err)
goto out;
ctl_buf = msg_sys->msg_control;
ctl_len = msg_sys->msg_controllen;
} else if (ctl_len) {
BUILD_BUG_ON(sizeof(struct cmsghdr) !=
CMSG_ALIGN(sizeof(struct cmsghdr)));
if (ctl_len > sizeof(ctl)) {
ctl_buf = sock_kmalloc(sock->sk, ctl_len, GFP_KERNEL);
if (ctl_buf == NULL)
goto out;
}
err = -EFAULT;
/*
* Careful! Before this, msg_sys->msg_control contains a user pointer.
* Afterwards, it will be a kernel pointer. Thus the compiler-assisted
* checking falls down on this.
*/
if (copy_from_user(ctl_buf,
(void __user __force *)msg_sys->msg_control,
ctl_len))
goto out_freectl;
msg_sys->msg_control = ctl_buf;
}
msg_sys->msg_flags = flags;

if (sock->file->f_flags & O_NONBLOCK)
msg_sys->msg_flags |= MSG_DONTWAIT;
/*
* If this is sendmmsg() and current destination address is same as
* previously succeeded address, omit asking LSM's decision.
* used_address->name_len is initialized to UINT_MAX so that the first
* destination address never matches.
*/
if (used_address && msg_sys->msg_name &&
used_address->name_len == msg_sys->msg_namelen &&
!memcmp(&used_address->name, msg_sys->msg_name,
used_address->name_len)) {
err = sock_sendmsg_nosec(sock, msg_sys);
goto out_freectl;
}
err = sock_sendmsg(sock, msg_sys);
/*
* If this is sendmmsg() and sending to current destination address was
* successful, remember it.
*/
if (used_address && err >= 0) {
used_address->name_len = msg_sys->msg_namelen;
if (msg_sys->msg_name)
memcpy(&used_address->name, msg_sys->msg_name,
used_address->name_len);
}

out_freectl:
if (ctl_buf != ctl)
sock_kfree_s(sock->sk, ctl_buf, ctl_len);
out:
return err;
}

看完了__sys_sendmsg,我们可以确定,无论是哪个发送数据的系统调用,最终都会调用到sock_sendmsg。下面是sock_sendmsg的相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int sock_sendmsg(struct socket *sock, struct msghdr *msg)
{
int err = security_socket_sendmsg(sock, msg,
msg_data_left(msg));

return err ?: sock_sendmsg_nosec(sock, msg);
}
static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)
{
int ret = INDIRECT_CALL_INET(sock->ops->sendmsg, inet6_sendmsg,
inet_sendmsg, sock, msg,
msg_data_left(msg));
BUG_ON(ret == -EIOCBQUEUED);
return ret;
}

到此,我们完成了数据包从用户空间到内核空间的流程跟踪。接下来的数据包发送过程,将根据不同的协议,走不同的流程。

数据报文的接收

系统调用接口

1
2
3
4
5
6
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

与send类似,recv一般也是面向连接的套接字。原因在于,对于非面向连接的套接字来说,若使用recv接收数据,通过该接口将不能获得发送端的地址,也就是说不知道这个数据是谁发过来的。所以,如果使用者不关心发送端信息,或者该信息可以从数据中获得,那么recv接口同样也可以用于非面向连接的套接字。再来看看recvfrom,它会通过额外的参数src_addr和addrlen,来获得发送方的地址,其中需要注意的是addrlen,它既是输入值又是输出值。最后是recvmsg,它与sendmsg一样,把接收到的数据和地址都保存在了msg中。其中msg.msg_name和msg.msg_len用于保存接收端地址,而msg.msg_iov用于保存接收到的数据。这三个系统调用与对应的发送接口一样,都支持设置标志位flags,都是比较现代的接口设计方法。

数据包从内核空间到用户空间的流程

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
SYSCALL_DEFINE4(recv, int, fd, void __user *, ubuf, size_t, size,
unsigned int, flags)
{
return __sys_recvfrom(fd, ubuf, size, flags, NULL, NULL);
}
SYSCALL_DEFINE6(recvfrom, int, fd, void __user *, ubuf, size_t, size,
unsigned int, flags, struct sockaddr __user *, addr,
int __user *, addr_len)
{
return __sys_recvfrom(fd, ubuf, size, flags, addr, addr_len);
}
int __sys_recvfrom(int fd, void __user *ubuf, size_t size, unsigned int flags,
struct sockaddr __user *addr, int __user *addr_len)
{
struct socket *sock;
struct iovec iov;
struct msghdr msg;
struct sockaddr_storage address;
int err, err2;
int fput_needed;

// 将用户空间的缓冲区导入到内核空间的 iovec 结构中
err = import_single_range(READ, ubuf, size, &iov, &msg.msg_iter);
if (unlikely(err))
return err;

// 查找文件描述符对应的 socket
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
goto out;

// 初始化消息头结构
msg.msg_control = NULL;
msg.msg_controllen = 0;
// 如果需要返回地址,则设置 msg_name 指向 address
msg.msg_name = addr ? (struct sockaddr *)&address : NULL;
msg.msg_namelen = 0;
msg.msg_iocb = NULL;
msg.msg_flags = 0;

// 如果 socket 是非阻塞的,则设置 MSG_DONTWAIT 标志
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;

// 接收消息
err = sock_recvmsg(sock, &msg, flags);

// 如果接收成功并且需要返回地址
if (err >= 0 && addr != NULL) {
// 将内核空间的地址移动到用户空间
err2 = move_addr_to_user(&address, msg.msg_namelen, addr, addr_len);
if (err2 < 0)
err = err2; // 如果移动失败,返回错误
}

// 释放 socket 引用
fput_light(sock->file, fput_needed);
out:
return err;
}
int sock_recvmsg(struct socket *sock, struct msghdr *msg, int flags)
{
int err = security_socket_recvmsg(sock, msg, msg_data_left(msg), flags);

return err ?: sock_recvmsg_nosec(sock, msg, flags);
}
static inline int sock_recvmsg_nosec(struct socket *sock, struct msghdr *msg,
int flags)
{
return INDIRECT_CALL_INET(sock->ops->recvmsg, inet6_recvmsg,
inet_recvmsg, sock, msg, msg_data_left(msg),
flags);
}

recvmsg 函数最后也会进入sock_recvmsg_nosec,后面的接收流程就要依赖于具体的协议实现了。。

标准库

连接到建立

在 Linux 网络编程中,bindconnectlistenaccept 是用于网络套接字编程的关键系统调用。它们在服务器和客户端通信中扮演着重要的角色。以下是这些函数的详细用法:

bind 函数

用途

bind 函数将套接字与特定的IP地址和端口号绑定在一起。

函数原型

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

参数

  • sockfd:套接字文件描述符,通过 socket() 创建。
  • addr:指向特定协议地址的指针,通常是 struct sockaddr_instruct sockaddr_in6struct sockaddr_un
  • addrlen:地址的长度,以字节为单位。

返回值

  • 成功:返回 0。
  • 失败:返回 -1,并设置 errno

示例

1
2
3
4
5
6
7
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT); // 指定端口
server_addr.sin_addr.s_addr = INADDR_ANY; // 服务器上所有可用接口

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));

connect 函数

用途

connect 函数用于客户端向服务器发起连接。

函数原型

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

参数

  • sockfd:套接字文件描述符。
  • addr:指向服务器地址的指针。
  • addrlen:地址的长度。

返回值

  • 成功:返回 0。
  • 失败:返回 -1,并设置 errno

示例

1
2
3
4
5
6
7
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));

listen 函数

用途

listen 函数用于将绑定到特定端口的套接字转换为一个被动套接字,以便接受来自客户端的连接请求。

函数原型

1
int listen(int sockfd, int backlog);

参数

  • sockfd:套接字文件描述符。
  • backlog:等待连接队列的最大长度。

返回值

  • 成功:返回 0。
  • 失败:返回 -1,并设置 errno

示例

1
2
3
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
listen(sockfd, 5); // 最大连接数为 5

accept 函数

用途

accept 函数从连接请求队列中提取下一个连接请求,并为该连接返回一个新的套接字。

函数原型

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

参数

  • sockfd:监听套接字的文件描述符。
  • addr:用于返回发起连接请求的实体的地址。
  • addrlen:指向地址长度的指针。

返回值

  • 成功:返回一个新的套接字文件描述符(用于与客户端通信)。
  • 失败:返回 -1,并设置 errno

示例

1
2
3
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int new_sockfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len);

使用这些函数,您可以创建一个基本的 TCP 服务器和客户端模型。服务器负责监听和接受客户端连接,而客户端则主动连接到服务器。

accept4 是 Linux 特定的系统调用,类似于 accept 函数,但它具有更多的灵活性,因为它允许在接受连接时指定额外的套接字标志。这种功能可以帮助在创建新套接字时避免竞争条件(race condition)或减少系统调用。accept4 是 GNU C Library (glibc) 的一部分,在一些 Unix-like 操作系统上可用。

accept4 函数

用途

accept4 函数用于从连接请求队列中提取下一个连接请求,并返回一个新的套接字,同时可以指定标志来设置新套接字的特性,比如非阻塞方式或关闭执行标志。

函数原型

1
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);

参数

  • sockfd:监听套接字的文件描述符。
  • addr:用于存储发起连接请求的客户端地址(可以为 NULL)。
  • addrlen:指向地址长度的指针,表示 addr 的空间大小,也用于返回实际地址的长度(可以为 NULL)。
  • flags:用于指定新套接字的属性标志,可以是以下标志的组合:
    • O_NONBLOCK:将套接字设置为非阻塞模式。
    • SOCK_CLOEXEC:设置关闭执行标志(close-on-exec),这对于多线程或 fork/exec 模型很有用。

返回值

  • 成功:返回一个新的套接字文件描述符(用于与客户端的通信)。
  • 失败:返回 -1,并设置 errno

示例

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
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h> // for O_NONBLOCK

int main() {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);

server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(8080);

bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
listen(sockfd, 5);

// 使用 accept4 接受连接,并指定非阻塞模式标志
int new_sockfd = accept4(sockfd, (struct sockaddr *)&client_addr, &client_len, O_NONBLOCK);
if (new_sockfd < 0) {
perror("accept4 failed");
}

// 在这里可以使用 new_sockfd 进行数据通信
// ...

close(new_sockfd);
close(sockfd);

return 0;
}

注意事项

  • accept4 提供的特性主要用于优化和简化套接字的使用过程,但在使用时需要检查平台是否支持此函数。
  • 如果你在一个不支持 accept4 的平台工作,可以通过 accept 和再调用 fcntl 来实现类似的功能。
  • 尽管 accept4 对应用程序性能和可靠性有帮助,要确保理解每个标志的影响,合理地组合使用。

报文的发送

在 Linux 网络编程中,sendsendtosendmsg 是用于发送数据的函数。虽然它们具有相似的功能,但每个函数针对不同的场景进行了优化和设计。

send 函数

用途

send 函数用于通过连接的 TCP 套接字发送数据。

函数原型

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

参数

  • sockfd:套接字文件描述符,必须是已连接的套接字。
  • buf:指向待发送数据缓冲区的指针。
  • len:要发送的数据长度。
  • flags:指定传输选项,通常为 0,也可以是以下一个或多个标志的组合:
    • MSG_DONTWAIT:非阻塞操作。
    • MSG_NOSIGNAL:避免在对等端崩溃的情况下发送 SIGPIPE 信号。

返回值

  • 成功:返回实际发送的字节数。
  • 失败:返回 -1,并设置 errno

示例

1
2
char *message = "Hello, World!";
int bytes_sent = send(sockfd, message, strlen(message), 0);

sendto 函数

用途

sendto 函数用于通过未连接的套接字(如 UDP)发送数据,并允许指定目标地址。

函数原型

1
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:传输选项,与 send 的类似。
  • dest_addr:指向目标地址的指针。
  • addrlen:目标地址的长度。

返回值

  • 成功:返回实际发送的字节数。
  • 失败:返回 -1,并设置 errno

示例

1
2
3
4
5
6
7
struct sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(PORT);
inet_pton(AF_INET, "127.0.0.1", &dest_addr.sin_addr);

char *message = "Hello, UDP World!";
int bytes_sent = sendto(sockfd, message, strlen(message), 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr));

sendmsg 函数

用途

sendmsg 函数是一种高级接口,用于发送带辅助数据的消息,也可以用于原始套接字或发送带有控制信息的数据包,可以用于已连接和未连接的套接字,已连接套接字不需要目标地址。

函数原型

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

参数

  • sockfd:套接字文件描述符。
  • msg:指向 msghdr 结构的指针,该结构包含要发送的数据和可选的地址和控制信息。
  • flags:传输选项,与 send 的类似。

msghdr 结构

1
2
3
4
5
6
7
8
9
struct msghdr {
void *msg_name; // 目标地址
socklen_t msg_namelen; // 地址长度
struct iovec *msg_iov; // I/O 向量数组
size_t msg_iovlen; // I/O 向量中的元素数
void *msg_control; // 整体附属数据的开始指针
size_t msg_controllen; // 附属数据的大小
int msg_flags; // 消息标志
};

返回值

  • 成功:返回实际发送的字节数。
  • 失败:返回 -1,并设置 errno

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct msghdr msg;
struct iovec iov[1];
char *message = "Hello, Advanced World!";

// 设置消息中的I/O向量
iov[0].iov_base = message;
iov[0].iov_len = strlen(message);

// 设置msghdr结构
msg.msg_name = NULL; // 不指定目标地址
msg.msg_namelen = 0;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
msg.msg_control = NULL;
msg.msg_controllen = 0;
msg.msg_flags = 0;

// 发送消息
int bytes_sent = sendmsg(sockfd, &msg, 0);

这三个函数分别用于在不同的场景中发送数据:send 用于简单的已连接套接字,sendto 用于未连接的套接字,sendmsg 则用于更复杂的消息结构和控制信息。选择合适的函数取决于您的应用程序需求。

报文的接收

在 Linux 网络编程中,recvrecvfromrecvmsg 是用于接收数据的函数。每个函数都有其特定的用途,根据不同的网络通信需求和场景进行设计。

recv 函数

用途

recv 函数用于从已连接的套接字接收数据,通常用于 TCP 连接。

函数原型

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

参数

  • sockfd:套接字文件描述符,必须是已连接的。
  • buf:指向存储接收数据的缓冲区的指针。
  • len:要接收的最大字节数(缓冲区大小)。
  • flags:指定接收选项,通常为 0,但可以是以下一个或多个标志的组合:
    • MSG_DONTWAIT:非阻塞操作。
    • MSG_PEEK:查看数据而不将其从队列中移除。
    • MSG_WAITALL:等待完整请求数据到达。

返回值

  • 成功:返回实际接收到的字节数。
  • 失败:返回 -1,并设置 errno

示例

1
2
char buffer[1024];
int bytes_received = recv(sockfd, buffer, sizeof(buffer), 0);

recvfrom 函数

用途

recvfrom 函数用于从未连接的套接字(如 UDP)接收数据,并可以获取数据来源的地址。

函数原型

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

参数

  • sockfd:套接字文件描述符,可以是已连接或未连接的。
  • buf:指向存储接收数据的缓冲区的指针。
  • len:要接收的最大字节数。
  • flags:接收选项,与 recv 的类似。
  • src_addr:指向用于存储源地址的 sockaddr 结构。
  • addrlen:指向 src_addr 的长度,调用后返回实际地址长度。

返回值

  • 成功:返回实际接收到的字节数。
  • 失败:返回 -1,并设置 errno

示例

1
2
3
4
struct sockaddr_in src_addr;
socklen_t addrlen = sizeof(src_addr);
char buffer[1024];
int bytes_received = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&src_addr, &addrlen);

recvmsg 函数

用途

recvmsg 提供了一个高级接口,用于接收包含辅助数据的消息,适用于需要处理控制信息或者更复杂协议的场景。

函数原型

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

参数

  • sockfd:套接字文件描述符。
  • msg:指向 msghdr 结构的指针,包含接收的数据和可选控制信息。
  • flags:接收选项,与 recv 的类似。

msghdr 结构

1
2
3
4
5
6
7
8
9
struct msghdr {
void *msg_name; // 存储源地址
socklen_t msg_namelen; // 地址长度
struct iovec *msg_iov; // I/O 向量数组
size_t msg_iovlen; // 向量的元素数
void *msg_control; // 用于存储辅助数据
size_t msg_controllen; // 辅助数据的大小
int msg_flags; // 消息标志
};

返回值

  • 成功:返回实际接收到的字节数。
  • 失败:返回 -1,并设置 errno

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct msghdr msg;
struct iovec iov[1];
char buffer[1024];

// 设置接收的数据缓冲区
iov[0].iov_base = buffer;
iov[0].iov_len = sizeof(buffer);

// 设置 msghdr 结构
msg.msg_name = NULL; // 如果不需要来源地址
msg.msg_namelen = 0;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
msg.msg_control = NULL;
msg.msg_controllen = 0;
msg.msg_flags = 0;

// 调用 recvmsg
int bytes_received = recvmsg(sockfd, &msg, 0);
  • Title: Linux环境编程与内核之网络通信
  • Author: 韩乔落
  • Created at : 2025-02-06 14:49:47
  • Updated at : 2025-03-18 11:06:16
  • Link: https://jelasin.github.io/2025/02/06/Linux环境编程与内核之网络通信/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments