Socket 与 TCP 三次握手

Demon.Lee 2022年06月26日 1,260次浏览

本文实践环境:
Operating System: macOS 12.4
Kernel: Darwin Kernel Version 21.5.0
Architecture: ARM64

其他环境可能有些许差异,比如 socket 相关的宏定义等。

学以致用,学习的目的是为了应用。如果可以边学边用,岂不是更好?在前面的文章中,笔者也提到过,网络协议是一门偏理论的内容,借助实践将会有助于我们理解。更主要的是,实践会给我们带来正反馈,而且是立刻反馈,有了反馈,我们就更不会轻易放弃理论的学习。

TCP 连接的三次握手,四次挥手大家可能都了解过,但这些是如何触发的呢?在这篇文章中,笔者就通过操作系统内核暴露出来的 Socket API,结合程序示例和抓包, 来梳理 TCP 连接的发起过程。

什么是 Socket

说到 Socket,第一个问题浮现在我们眼前的就是:什么是 Socket?

Socket 在英文中的原意是“插口”,“插槽”,中文翻译为套接字,不过笔者跟一些网友一样,认为这个翻译等于没翻译。

从“插口”,“插槽”的原意,我们可以很容易联想到,这个插座就是电源系统暴露的一个接口。同样类比,计算机网络通信中的建立连接,发送消息(图片,语音,文字等等),接收消息等,都是借助 Socket 来完成的。

如果还觉得比较抽象,我们再来看看其他的解释:

  • 维基百科[1]上的定义:

    A network socket is a software structure within a network node of a computer network that serves as an endpoint for sending and receiving data across the network. The structure and properties of a socket are defined by an application programming interface (API) for the networking architecture. Sockets are created only during the lifetime of a process of an application running in the node.

  • Oracle官网[2]也有一个定义:

    A socket is one endpoint of a two-way communication link between two programs running on the network. A socket is bound to a port number so that the TCP layer can identify the application that data is destined to be sent to.

在这两个解释中,都出现了 endpoint 这个词,也就是“端点”,这是因为通信一般都有两端,发起方和接收方,两端都要有 Socket 才能通信。我们可以把 Socket 理解为计算机通信中的一个抽象模型,通过这个模型来快速完成网络连接和数据收发,这就相当于 Socket 为网络通信的应用程序提供了接入 API。

说完了 Socket 的基本概念,下面简单聊一聊 Socket 和网络编程的发展史。

Socket 可以追溯到 1971 年 RFC 147 的发布,当时它被用于ARPANET,而大多数现代 Socket 的实现都是基于加州大学伯克利分校提出的 Socket(1983)。伯克利 Socket API 最早在 BSD 4.2 Unix 内核上实现,并且实现第一版 Socket 的就是 TCP/IP 协议(这不禁让人想起天才程序员 Bill Joy[3])。然而,只有在 1989 年,加州大学伯克利分校才能发布其操作系统和网络库的版本,摆脱 AT&T 版权保护的 Unix 许可限制。

Linux 很早就从头开发实现了 TCP/IP 协议,Windows 后续也跟进引入了 Socket 概念。在今天的互联网世界里,Socket 已经成为网络互联互通的标准。下图来自网络课程《网络编程实战》,笔者对其进行了重新绘制。

TCP/IP Evolution History

Socket 编程

结合前面对 Socket 的基本概述,下面通过一个 TCP 客户端——服务器回写消息的小程序来直观体验一下,如何使用 Socket API 编程。

// echo_client.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include "common/log.h"
#include "common/general.h"

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

    // (1) socket
    int client_socket = socket(PF_INET, SOCK_STREAM, 0);
    if (-1 == client_socket) {
        error_handling("create socket failed");
    }

    struct sockaddr_in server_addr;
    memset(&server_addr, '\0', sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(argv[1]);
    server_addr.sin_port = htons(atoi(argv[2]));

    // (2) connect
    int connect_ret = connect(client_socket, (struct sockaddr *) &server_addr, sizeof(server_addr));
    if (-1 == connect_ret) {
        error_handling("connect socket failed");
    } else {
        printf("connect server success...\n");
    }

    char message[BUF_SIZE];
    int message_len = 0, recv_len = 0, read_len = 0;
    while (1) {
        fputs("Input(Q to quit): ", stdout);
        fgets(message, BUF_SIZE, stdin);
        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")) {
            printf("quit...\n");
            break;
        }

        // (3) write
        message_len = strlen(message);
        if (message_len != write(client_socket, message, strlen(message))) {
            printf("write message error, quit...\n");
            break;
        }

        recv_len = 0;
        // using '<' instead of '!=': prevent endless loop
        while (recv_len < message_len) {
            // (4) read
            read_len = read(client_socket, &message[recv_len], BUF_SIZE);
            if (-1 == read_len) {
                error_handling("read from server failed");
            } else if (0 == read_len) {
                error_handling("server closed");
            }
            recv_len += read_len;
        }

        message[message_len] = '\0';
        printf("Message from server: %s", message);
    }

    // (5) close
    close(client_socket);

    return 0;
}
// echo_server.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#include "common/log.h"
#include "common/general.h"

#define CONNECTION_SIZE 5

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

    // (1) socket
    int server_socket = socket(PF_INET, SOCK_STREAM, 0);
    if (-1 == server_socket) {
        error_handling("create socket failed");
    }

    struct sockaddr_in server_addr;
    memset(&server_addr, '\0', sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(atoi(argv[1]));

    // (2) bind
    int bind_ret = bind(server_socket, (struct sockaddr *) &server_addr, sizeof(server_addr));
    if (-1 == bind_ret) {
        error_handling("bind socket failed");
    }

    // (3) listen
    int listen_ret = listen(server_socket, CONNECTION_SIZE);
    if (-1 == listen_ret) {
        error_handling("listen socket failed");
    }

    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    int client_socket, message_len;
    char message[BUF_SIZE];

    for (int i = 0; i < CONNECTION_SIZE; ++i) {
        memset(&client_addr, '\0', sizeof(client_addr));
        printf("waiting connection to accept...\n");
        
        // (4) accept
        client_socket = accept(server_socket, (struct sockaddr *) &client_addr, &client_addr_len);
        if (-1 == client_socket) {
            error_handling("accept socket failed");
        } else {
            printf("Connected client[%d]: %s:%d\n", i, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
        }

        while (1) {
            // (5) read
            message_len = read(client_socket, message, BUF_SIZE);
            if (message_len <= 0) {
                if (-1 == message_len) {
                    printf("read from client[%d] error: [%s]\n", i, strerror(errno));
                } else {
                    printf("client[%d] closed\n", i);
                }
                
                // (7) close socket that binds the client
                close(client_socket);
                break;
            }
            
            // (6) write
            if (message_len != write(client_socket, message, message_len)) {
                printf("write to client[%d] failed: [%s]\n", i, strerror(errno));
            }
        }
    }

    // (8) close server listen socket
    close(server_socket);

    return 0;
}

使用 gcc 对其进行编译,然后运行示例(先启动服务端),如下所示:

# echo_server

➜  build git:(main) > ./bin/echo_server 11233          
waiting connection to accept...
Connected client[0]: 127.0.0.1:59694
client[0] closed
waiting connection to accept...
➜  build git:(main) >
# echo_client

➜  build git:(main) >./bin/echo_client 127.0.0.1 11233
connect server success...
Input(Q to quit): 你好,Socket!
Message from server: 你好,Socket!
Input(Q to quit): q
quit...
➜  build git:(main) >

通过程序,笔者来简单梳理一下大致的步骤:

  • 服务端启动,准备数据,等待客户端连接
  • 客户端发起连接
  • 服务端接收客户端连接,双方连接建立
  • 客户端发送数据(“你好,Socket!”)
  • 服务端收到数据
  • 服务端将数据再写回客户端
  • 客户端输入“q”,关闭连接
  • 服务端收到关闭消息,也关闭连接

有了上面的简单梳理,相信大家对整个流程有了一定的了解。细心的读者可能会发现,在上述代码中,笔者将 Socket 编程中主要的步骤进行了标注,如 // (1) socket。通过这些关键步骤,我们对使用 Socket 进行 TCP 编程的流程图也就呼之而出了:

TCP/IP Evolution History

根据这幅图,笔者对相关函数做一个解释,以此来深入理解 Socket。另外,在谈 Socket 编程时,会有一个打电话的经典示例,笔者这里也将其引用过来,帮助理解。

TCP/IP Evolution History

服务端准备连接

1、socket()

创建 Socket,类比于安装电话机,服务端和客户端都需要调用。

NAME
     socket – create an endpoint for communication

SYNOPSIS
     #include <sys/socket.h>

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

DESCRIPTION
     socket() creates an endpoint for communication and returns a descriptor.

参数 domain:表示 Socket 使用的协议族(Protocol Family),下表列出了部分协议族类型:

名称 协议族
PF_INET IPv4 互联网协议族
PF_INET6 IPv6 互联网协议族
PF_LOCAL 本地通信的 UNIX 协议族
PF_IPX IPX Novell 协议族

参数 type:表示 Socket 使用的数据传输类型,总结如下表所示:

名称 类型 描述
SOCK_STREAM 字节流,面向连接 可靠、有序、基于字节、面向连接,传输的数据不存在边界
SOCK_DGRAM 数据报,面向消息 不可靠、无序、无连接,传输的数据有大小限制,所以存在边界
SOCK_RAW 原始 Socket 原始网络协议处理,不会经过 TCP/IP 协议层,故要应用程序自己解析[4]

参数 protocol:用于指定通信协议,但现在的情况是通过前面两个参数已经可以确定某个通信协议了,比如 IPv4 下的 SOCK_STREAM 目前只有 TCP 协议,所以该参数一般都传 0,即下面两种方式是等价的:

int server_socket = socket(PF_INET, SOCK_STREAM, 0);

int server_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

如果 IPv4 下的 SOCK_STREAM 下还有多种协议,那么就需要通过第三个参数指定了。

返回值:成功返回文件描述符,失败返回 -1。

2、bind()

将创建好的 Socket 与地址(IP 以及端口等)绑定,相当于让通信服务商给电话机分配一个号码,服务端和客户端都可以使用。

NAME
     bind – bind a name to a socket

SYNOPSIS
     #include <sys/socket.h>

     int
     bind(int socket, const struct sockaddr *address, socklen_t address_len);

DESCRIPTION
     bind() assigns a name to an unnamed socket. 
     When a socket is created with socket(2) it exists in a name space (address family) but has no name assigned. 
     bind() requests that address be assigned to the socket.

NOTES
     Binding a name in the UNIX domain creates a socket in the file system that must be deleted by the caller
     when it is no longer needed (using unlink(2)).

RETURN VALUES
     Upon successful completion, a value of 0 is returned. 
     Otherwise, a value of -1 is returned and the global integer variable errno is set to indicate the error.

参数 socket:即前面通过 socket() 创建的 Socket 文件描述符。

参数 address:Socket 格式地址;参数 address_len:Socket 地址长度。

这里重点说明一下 struct sockaddr,即 Socket 格式地址。

(1)sockaddr:通用 Socket 格式地址

 /* POSIX.1g 规范规定了地址族为2字节的值. */
 typedef __uint8_t               sa_family_t;

 // 共 16 字节
 struct sockaddr {
 	__uint8_t       sa_len;         /* total length */
 	sa_family_t     sa_family;      /* [XSI] address family */
 	char            sa_data[14];    /* [XSI] addr value (actually larger) */
 };

sa_len 表示结构体的总长度,在 Linux 下可能不存在该字段,只是 macOS 下的实现有。

sa_family 即为协议地址族,AF_INET 表示 IPv4 网络协议中使用的地址族,AF_INET6 表示 IPv6 网络协议中使用的地址族,而 AF_LOCAL 则表示本地通信中使用的地址族。咦,有没有发现与上面的 PF_INET,PF_INET6,PF_LOCAL 很像,其实:

// <sys/socket.h>

#define PF_UNSPEC       AF_UNSPEC
#define PF_LOCAL        AF_LOCAL
#define PF_UNIX         PF_LOCAL        /* backward compatibility */
#define PF_INET         AF_INET
#define PF_INET6        AF_INET6
// ...
// ...

sa_data 表示实际的地址值。

(2)sockaddr_in:IPv4 Socket 格式地址

/* POSIX.1g 规范规定了地址族为2字节的值. */
typedef __uint8_t               sa_family_t;

/* 端口两个字节 */
typedef __uint16_t              in_port_t;

/*
 * Internet address (a structure for historical reasons)
 */
typedef __uint32_t      in_addr_t;  /* base type for internet address, 4 bytes*/

struct in_addr {
	in_addr_t s_addr;
};

// 共 16 字节
struct sockaddr_in {
	__uint8_t       sin_len;
	sa_family_t     sin_family;
	in_port_t       sin_port;
	struct  in_addr sin_addr;

	/* 占位符,不起作用 */
	char            sin_zero[8];
};

可以看到,与上面通用格式地址相比,IPv4 Socket 格式地址使用起来更友好,端口及 IP 地址都有对应的字段。不过需要注意的是,这里的地址用的是 int 类型,而不是我们平时直观的 “192.168.1.22” 这样用“.”分割的形式。

上述代码示例中的实现如下:

server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(atoi(argv[1]));

这里有一个问题,为何要使用 htonl 以及 htons 等函数转换,比如端口为啥不能直接赋值?

这涉及到一个网络字节序的问题。因为不同的 CPU 体系,数据存储的方式是不同的,像 AMD/Intel CPU 都使用小端序,而网络传输则要求统一用大端序,所以这里就有一个字节序转换问题。关于字节序的更多信息,笔者后续有机会再进行总结。而前不久阮一峰老师就写了一篇《字节序探析:大端与小端的比较》,推荐阅读。

htnol 或 htons 中的 h 表示 host,即主机字节序;n 表示 network,即网络字节序;l 表示 long,s 则表示 short。
所以,htons 可以理解为:把 short 类型数据从主机字节序转换为网络字节序。

上面地址中的 INADDR_ANY 宏定义 #define INADDR_ANY (u_int32_t)0x00000000 则表示不指定固定 IP,使用通配地址,即该程序可以从当前机器上不同的 IP 地址接收数据。而如果需要绑定固定 IP,比如 192.168.1.22,可以使用 inet_addr() 函数,如下所示:

server_addr.sin_addr.s_addr = inet_addr("192.168.1.22");

inet_addr() 函数不仅能转换地址,而且返回的值直接就是具备大端序的 Internet address。

(3)sockaddr_in6:IPv6 Socket 格式地址

typedef __uint8_t               sa_family_t;
typedef __uint16_t              in_port_t;

typedef struct in6_addr {
union {
		__uint8_t   __u6_addr8[16];
		__uint16_t  __u6_addr16[8];
		__uint32_t  __u6_addr32[4];
    } __u6_addr;                    /* 128-bit IP6 address */
} in6_addr_t;

// 共 28 字节
struct sockaddr_in6 {
	__uint8_t       sin6_len;       /* length of this struct(sa_family_t) */
	sa_family_t     sin6_family;    /* AF_INET6 (sa_family_t) */
	in_port_t       sin6_port;      /* Transport layer port # (in_port_t) */
	__uint32_t      sin6_flowinfo;  /* IP6 flow information */
	struct in6_addr sin6_addr;      /* IP6 address */
	__uint32_t      sin6_scope_id;  /* scope zone index */
};

由于前面已经对相关字段进行了说明,所以这里就不再赘述,IPv6 不同的字段主要是 sin6_flowinfo,sin6_addr 和 sin6_scope_id,分别代表 IPv6 的流控信息,地址信息和域 ID。

(4)sockaddr_un:本地 Socket 格式地址

typedef __uint8_t               sa_family_t;

// 共 106 字节
struct  sockaddr_un {
	unsigned char   sun_len;        /* sockaddr len including null */
	sa_family_t     sun_family;     /* [XSI] AF_UNIX */
	char            sun_path[104];  /* [XSI] path name (gag) */
}

sockaddr_un 主要不同的字段为 sun_path,该字段用来存储本地通信依赖的文件路径名(全路径)。

好,到这里几大类 Socket 地址格式都进行了一个简单梳理,那么通用地址格式与它们是什么关系呢?其实,如果细心观察前面的代码示例,应该能猜出一二了。

int bind_ret = bind(server_socket, (struct sockaddr *) &server_addr, sizeof(server_addr));
if (-1 == bind_ret) {
    error_handling("bind socket failed");
}

bind() 函数的入参需要的是 const struct sockaddr *address,所以这里就有了一个类型转换的过程。我们可以将 sockaddr 当作抽象类,而 sockaddr_in,sockaddr_in6 等就是具体的实现类了,这其实就是 socket 编程中的多态。下图对几类地址格式进行了总结:


需要再次强调的是,这里依据的是 macOS 12.4 上相关函数的定义,如果是 Linux OS ,则可能存在细节差异。

3、listen()

为接收用户请求做好就绪准备,比如完成连接队列,类比于给电话接上电话线,服务端调用。

NAME
     listen – listen for connections on a socket

SYNOPSIS
     #include <sys/socket.h>

     int listen(int socket, int backlog);

RETURN VALUES
     The listen() function returns the value 0 if successful; otherwise the
     value -1 is returned and the global variable errno is set to indicate the
     error.

参数 socket:即前面创建的 Socket 的文件描述符。

参数 backlog:backlog参数定义了等待连接队列(指已完成 ESTABLISHED 但还未 accept 的队列)的最大长度,也就是最大并发连接数。 如果一个连接请求到达时队列已满,客户端可能会收到一个错误,指示为 ECONNREFUSED。当然,如果底层协议支持重传,请求可以被忽略,这样重试就会成功。

4、accept()

接收连接,类比于电话铃响了,服务端调用。

NAME
     accept – accept a connection on a socket

SYNOPSIS
     #include <sys/socket.h>

     int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);

RETURN VALUES
     The call returns -1 on error and the global variable errno is set to
     indicate the error.  If it succeeds, it returns a non-negative integer that
     is a descriptor for the accepted socket.

一般情况下,如果没有请求到来,那么服务端主程序将会阻塞在 accept 函数上。当有请求时,accept() 会开始执行处理,并创建一个新的 Socket,即与客户端进行一一对应处理的 Socket。这个新 Socket 的地址值(即客户端的 IP 和 端口等)就存储在第 2 个参数 address 中,而第 3 个参数 address_len 则是它的长度。

这里为什么要搞两个 Socket 呢,一个不行吗?因为一开始创建的 Socket 是作为监听使用的,用来接收成千上万的客户端请求,如果一次只处理一个,那得需要多少服务器才能满足大量的并发请求呢?如果用下表来表示,就能比较容易理解了:

Socket 客户端 IP 客户端 Port 服务端 IP 服务端 Port
ListenSocket * * 192.168.1.22 11233
ClientSocket1 192.168.1.23 32111 192.168.1.22 11233
ClientSocket2 192.168.1.110 56781 192.168.1.22 11233
192.168.1.22 11233

客户端发起连接

相较于服务端准备连接的过程,客户端发起连接的过程要简单的多,创建 Socket 成功之后,直接调用 connect() 函数就可以了。

5、connect()

发起连接,类比于打电话,客户端调用。

NAME
     connect – initiate a connection on a socket

SYNOPSIS
     #include <sys/types.h>
     #include <sys/socket.h>

     int connect(int socket, const struct sockaddr *address, socklen_t address_len);

RETURN VALUES
     Upon successful completion, a value of 0 is returned.  Otherwise, a value
     of -1 is returned and the global integer variable errno is set to indicate
     the error.

如果仔细观察,我们会发现,这里的第 2,3 两个参数其实指的是服务端的 Socket 地址以及其长度。那客户端的地址呢,又用哪个端口发起的请求?

其实,这个 connect() 函数做的事情远比我们想象的要多,如果客户端程序没有调用 bind() 函数,那么 connect 函数(即内核)会确定源 IP 地址,并按照一定的算法选择一个临时端口作为源端口。

如果是 TCP 连接,那么 connect() 函数另外一件非常重要的事情,就是进行 TCP 三次握手,也就是客户端-服务器双方需要建立一些数据结构来维护连接状态。

TCP 三次握手

上面提到,在 TCP 协议中,客户端使用 connect() API 会触发三次握手动作,那怎么证明呢?

其实很简单,抓包。下图便是用 Wireshark 对上述代码示例进行抓包的结果:


这里我们重点看前面 3 个包,这就是著名的 TCP 三次握手。从抓包可以看到,源端口取的值为 59694,而目的端口则为 11233。通过一幅交互图,我们来理一理这个交互过程:


首先一个问题是:为什么握手是三次,可不可以是两次,四次,五次,甚至六次呢?

这是因为双方通信至少需要一个来回(即发出去的消息得有反馈),才能确定对方是否在线。比如 A 和 B 通信:

A:你好 B,我是 A。
B:收到。
B:你好 A,我是 B。
A:收到。

这样一来,就需要四次消息了,但我们完全可以把第二、三两次进行合并,所以就变成三次消息了。但三次握手是否就能保证连接正常呢?不能。因为最后 A 收到消息后,可能又挂了,所以四次、五次、六次都是可以的,但这依然不能保证线路不存在问题。

这里有几个概念需要同步一下,SYN 包表示发起连接,ACK 包表示响应,而 seq 和 ack 则为具体的包序号,这个序号的作用就是为了保证 TCP 协议可靠、有序而设置的,比如有数据包丢失,将这个序号对应的包重发一遍就好了。另外,这个序号的初始值有算法保证,在一定时间段内不会重复,后面会根据传递的字节数,对序号进行累加。

回到上面的流程图,三次握手的状态变更可以总结为:

  • 服务端调用 listen() 函数后进入 LISTEN 状态,然后通过调用 accept() 函数阻塞;
  • 客户端调用 connect() 函数,发送 SYN 包,序列号 x,客户端进入 SYNC_SENT 状态,阻塞;
  • 服务端收到包后,回应一个 ACK 包,序列号 x+1,同时再发一个 SYN 包,序列号 y,服务端进入 SYNC_RCVD 状态;
  • 客户端收到 ACK 响应后,connect() 函数返回,状态变成 ESTABLISHED,表示客户端到服务端的单向连接建立完成。同时,客户端再给服务端回复一个 ACK 包,序号为 y+1;
  • 服务端收到 ACK 包后,从 accept() 函数返回,状态变成 ESTABLISHED,表示服务端到客户端的单向连接也建立完成,于是三次握手结束。

为了能看懂 Wireshark 数据包中的 TCP 头字段,我们需要了解 TCP 标准头定义:


这里,笔者对相关属性进行一个简单说明:

  • 序列号:前面已经提到过,它其实指的是发送数据的位置,每发送一次数据,序号累加一次,累加值即为数据的字节大小。需要说明的是,虽然 SYN 包和 FIN 包不携带数据,但也会作为一个字节增加对应的序列号。

  • 确认应答号:下一次应该收到的数据的序列号,其值减一就表示已经收到了多少数据。

  • 数据偏移:表示 TCP 传输的数据从 TCP 包的哪个位开始计算,其实就是 TCP 头部的长度,这个值对应的单位为 4 bytes,如果值为 11(二进制表示即为 1011),那么表示 TCP 头部的长度为 4 * 11=44 bytes。一般情况下,如果 TCP 头部没有选项字段的话,其长度为 5 * 4=20 bytes

  • 保留:用于以后扩展,一般长度为 4 bit。

  • 控制位:一般为 8 bit,从左到右分别是 CWR、ECE、URG、ACK、PSH、RST、SYN 和FIN,如果要设置某个位,将对应值置为 1 即可。

    FLAG 全称 描述
    CWR Congestion Window Reduced 与下面的 ECE 都用于 IP 头部的 ECN 字段,当 ECE 标志为 1 时,通知对方已将拥塞窗口缩小
    ECE ECN-Echo 置为 1 会通知对方,从对方到这边的网络有拥塞
    URG Urgent 若值为 1,则表示包中有需要紧急处理的数据,更多内容请参考后面的紧急指针选项
    ACK Acknowledgement 若值为 1,确认应答的字段变为有效。除了最初建立连接的 SYN 包外,该位必须设置为 1
    PSH Push 若值为 1,收到应用层消息立马发送,或将收到的数据立马传给应用层;若值为 0,则先缓冲
    RST Reset 若值为 1,表示 TCP 连接出现异常,必须强制断开
    SYN Synchronize 用于建立连接,值为 1 表示希望建立连接
    FIN Fin 该值若为 1,表示后面不再有数据包发送过来,希望断开连接
  • 窗口大小:接收窗口的大小,指定发送方目前愿意接收的窗口大小,即 TCP 不允许发送超过此处所示大小的数据。

  • 校验和:通过一定算法对接收到的数据包进行校验和计算,检查数据包是否因路由器内存故障或程序漏洞被破坏。

  • 紧急指针:上面控制位的 URG 置为 1 时才有效,用于指示当前数据包中紧急数据的结束位置,即 TCP 数据部分的首地址到该指针所指位置即为紧急数据。如何处理紧急数据,是应用程序的问题。一般在暂时中断通信,或中断通信的情况下使用。

  • 选项:该字段用于提高 TCP 的传输性能,下图来自《图解 TCP/IP(第5版)》,笔者这里直接进行引用:


    比如类型 2 的 MSS 用于设置建立连接时决定最大段长度的情况;类型 3 的窗口大小伸缩,用来改善 TCP 吞吐量;类型 8 的时间戳用于高速通信中对序列号的管理,防止新老序列号产生重复;类型 4 和 5 用于选择确认应答,避免无效的重发。

结合代码示例,以及 Wireshark 抓包再来理解 TCP 三次握手的基本流程,相信比只看书要更容易。由于 TCP 涉及的内容还有很多,比如断开连接时的四次挥手,数据包重传,滑动窗口,拥塞控制等等。在后续的文章中,笔者还会像这篇文章一样,理论结合实践,逐步对这些内容进行梳理和总结。

参考资料


  1. https://en.wikipedia.org/wiki/Network_socket ↩︎

  2. https://docs.oracle.com/javase/tutorial/networking/sockets/definition.html ↩︎

  3. https://www.fortunechina.com/magazine/c/1999-08/09/content_124.htm ↩︎

  4. https://stackoverflow.com/questions/30780082/sock-raw-option-in-socket-system-call ↩︎