Socket和TCP协议

阅读 173
标签: Python

本文将通过编写一个简单的TCP套接字程序来介绍TCP协议。为了更好地理解协议,我们还会使用WireShark这款工具来观察通信双方的行为。

什么是套接字

网络应用程序由成对的进程组成,一个进程位于服务器中,另外一个位于客户端中。从一个进程向另一个进程发送的消息需要借助网络来传递。进程就是通过套接字(Socket)向网络发送消息或从网络中接受消息。我们可以将进程想象为一个🏠,而它的套接字就是它的🚪。

当一个进程向另一台电脑上的进程发送消息时,它会把消息推出该门(套接字),消息在网络中进行传输,直到被送到目的进程的门口。目的进程的门(套接字)将接受该消息。之后,目的进程就可以对该消息进行处理了。

上图描述了两个进程通过因特网来进行网络通信(假设运输层协议用的是TCP)的过程。可以看到,Socket就是进程(应用层)和运输层之间的“桥梁”。

认识TCP

当我们在浏览器中浏览Web网站时,使用的网络协议就是http(s),http是应用层协议,其下层的运输层中使用的是TCP协议。TCP有两个主要特点:

  • 面向连接在正式传输数据之前,通信双方需要先建立一个通信链路。只有建立通信链路后,双方才能进行消息的收发。TCP使用“三次握手”来创建这个通信链路。为什么要这么麻烦呢?这主要是为后续的可靠传输奠定基础。连接建立的过程同步了双方的序列号,为数据包的顺序传输和确认做好了准备。
  • 可靠:指传递的消息是无差错、按顺序的,不会出现丢失消息或冗余消息的情况。

接下来,我们使用 Python3 来写一个简单的TCP服务端程序:

import socket

serverPort = 9999
serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

serverSocket.bind(("", serverPort))
serverSocket.listen(1)  # 等待队列最大长度为1
print("The server is ready")

while True:
    connectionSocket, addr = serverSocket.accept()
    print("Connection from: ", addr)

    msg = connectionSocket.recv(1024).decode()
    connectionSocket.send(msg.encode())
    connectionSocket.close()

我们将其保存到server.py文件中,并使用 python server.py 来启动它。

接着,我们启动WireShark,并加上筛选条件:tcp.port == 9999。这样,在WireShark中就只会显示在9999端口号上的TCP消息,方便我们查看。

现在,我们在另一台电脑(ip为192.168.3.59)上使用nc命令来连接这个TCP服务器:

nc -v 192.168.3.100 9999

正常情况下,会打印出“Ncat: Connected to 192.168.3.100:9999“,并等待用户输入。

请注意上面代码中的 serverSocket 和 connectionSocket的区别。你可以将serverSocket 想象为银行大门的接待员,专门负责迎接新客户。而 connectionSocket 就像银行柜员,专门为具体客户提供服务。接待员将客户引导到柜员那里,然后继续迎接下一位客户。

TCP三次握手

到这里为止,如果一切正常的话,我们会在WireShark中看到如下信息:

这就是TCP的三次握手过程,我们来逐一分析:

  • 首先,是第666号消息,ip为192.168.3.59的TCP客户端向TCP服务端发起连接请求,消息中包含了SYN(Synchronize)标记,表示想要和服务端建立连接。
  • 接着,是667号消息,TCP服务端向客户端返回了包含 [SYN, ACK] 标记的消息,表示“我同意建立连接,同时也想和你同步我的序列号”。
  • 最后,是668号消息,客户端发送了包含ACK标记的消息,表示对667号服务器消息的确认。

到此,三次握手过程完成,一条通信链路在双方之间建立了。之后,双方接发消息就可以通过这个通信链路来进行了。

另外,请思考一个问题:为什么需要三次握手,而不是2次呢?

这是一个有趣的问题。一个攀岩者和它的保护者之间就使用了类似的三次握手机制,以确保攀岩者在开始攀爬前双方都准备好了。

消息的传输

既然通信链路已经在双方建立,接下来就可以在这个通信链路上进行通信了。

注意:TCP是全双工的,也就是说,客户端和服务端双方都可以发送和接受消息。

我们来测试一下。

首先,我们在客户端电脑(ip为192.168.3.59)的终端上继续输入aa字符,按回车键后,在终端上打印出了aa字符(由TCP服务器返回)。

在WireShark中出现的抓包信息如下:

我们来逐一分析下:

  • 序号743:客户端向服务器发送携带PSH,ACK标记的消息,长度为3(两个字符串再加一个回车符)。
  • 序号744:服务器收到消息后,向客户端返回ACK标记的消息,表示“你这个信息我已经收到”。
  • 序号745:服务器主动向客户端发送消息,内容为“aa\n”,所以,Len=3。对应的代码为 connectionSocket.send(msg.encode())
  • 序号746:服务器关闭了这个通信链路,表示”我要关闭了“。消息中会携带FIN,ACK标记。
  • 序号749:客户端对服务器发送的序号745的回应。表示该条消息我已经收到。
  • 序号952:客户端对服务器发送的序号746的回应。表示该条消息我已经收到。

到这里为止,双方之间的这个通信链路已经关闭了。这时,我们在客户端再发送字符串b,并按下回车符。可以看到客户端向服务器发送的消息(序号为769)。然后,服务端返回了携带RST标记的消息,表示“这个连接已经不存在了,不要再发数据了。“

连接终止

正常来说,TCP连接的关闭需要进行四次握手。过程如下:

但是,为什么我们上面的WireShark抓包信息中只有两次握手呢?还有两次握手过程丢失了?

因为很多 TCP 客户端工具(例如 nc)在接收到服务器的 FIN 后,会马上调用 close() 关闭套接字。这时客户端的 FIN 和前一个 ACK 可能被合并(coalesce)在同一个 TCP 报文中或被操作系统快速处理完毕。因此你看到2个包而不是4个包,不代表TCP只用了两次挥手。只是后两步在非常短的时间内被系统“折叠”或“内联处理”掉了。

为了观察完整的四次握手过程,我们可以在客户端电脑的终端上按下Ctrl + D表示 EOF,即客户端主动关闭),然后,在WireShark中就可以看到完整的四次握手信息了。

如上图所示,这就是一个完整的四次握手信息。

参考文献

  • James F. Kurose, Keith W. Ross. Computer Networking, A Top-Down Approach, Sixth Edition. Pearson.
最后编辑于: 2025-10-20

评论(0条)

(必填)
复制成功