35. TCP网络编程

一、TCP协议简介

1.1、什么是TCP协议

  TCP 协议则是建立在 IP 协议之上的。TCP 协议负责在两台计算机之间建立可靠连接,保证数据包按顺序达到。TCP 协议会通过 3 次握手建立可靠连接。然后需要对每个 IP 包进行编号,确保对方按顺序收到,如果包丢了,就自动重发。一个 TCP 报文来了以后,到底是交给那个程序,就需要端口号来区分。每个网络程序都向操作系统申请一个端口号。这样,两个进程在两台计算机之间建立网络连接就需要各自的 IP 地址和各自的端口号。一个进程也可以能同时与多个计算机建立连接,因此它会申请多个端口。

TCP通信

TCP 每次发送一个数据包,对方都要进行确认,保证数据准确到达对象(不需要应用程序来做这件事,操作系统会自动来做);

TCP 通信的过程中,可以想象成这个虚拟的通道正在占用,不允许其它人来发送结束;

TCP 只需要建立一次连接,之后只需要发送数据,而不需要填写对象的 IP 和 PORT;

1.2、三次握手

  TCP 是稳定的传输方式,在接收、发送之前,双发需要建立一个虚拟的通道,这个过程称为 3 次握手。3 次握手的流程如下:

三次握手

  1. 客户端调用 connect() 时发送一个带有标记的数据包,我们把建立连接时的第 1 次数据叫做 SYN,其中由 1 个数字;
  2. 服务器接收到这个 SYN 数据包,提取出数字,然后 +1,回送给客户端。这个数据包中有 2 部分:SYN + ACK;
    1. ACK 是对接收到的数据的确认;
    2. SYN 表示要向客户端发送的数据;
  3. 当客户端接收到 SYN + ACK 数据包之后,提取数字,然后加 1,然后用 ACK 数据包回送给服务器;

当客户端调用 connect() 方法的时候,就有了 TCP 的 3 次握手,目的是让双方都分配一些资源(内存等)为将来进行网络通信时做准备;

服务器会阻塞到 accept() 方法这里,直到客户端发起连接,即 3 次握手完成之后,accept() 才会解阻塞,并且 accept() 返回一个新的套接字还有刚刚连接成功的 IP 和 PORT;

1.3、四次挥手

  为了释放资源,所以双方需要协商怎样关闭这个虚拟的通道,这就是 4 次挥手。4 次挥手的过程如下:

四次挥手

  1. 客户端先发送一个数据包,这里有 1 个数字,4 次挥手开始的第 1 次数据包称为 FIN;
  2. 服务器接收到 FIN 数据包,然后将数字提取出来,然后 +1,通过 ACK 数据包发送给客户端;
    1. 此时服务器的 recv() 会解阻塞,并且返回的数据长度为 0;
    2. 如果服务器对已经建立的套接字调用 close(),那么就会有下面的 2 次挥手;
  3. 服务器发送一个数据,这里有 1 个数字,这个包类型是 FIN;
  4. 当客户端接收服务器的 FIN 时,提取出数字,然后 +1,然后用 ACK 数据包回送给服务器;

当客户端调用 close() 方法时,操作系统会发起 TCP 的 4 次挥手;

当服务器调用 close() 方法时,才会发送第 3 次挥手数据;

二、TCP网络编程

2.1、创建TCP服务器

  创建 TCP 服务器的伪代码如下:

import socket                   # 导入socket模块

ss =  socket.socket()           # 创建服务器套接字
ss.bind()                       # 套接字与地址绑定
ss.listen()                     # 监听连接

while True:                     # 监听连接
    cs = ss.accept()            # 接收客户端连接
    while True:                 # 通信循环
        cs.recv()/cs.send()     # 对话(接收/发送)
    cs.close()                  # 关闭客户端套接字

ss.close()                      # 关闭服务器套接字

  所有套接字都是通过使用 socket.socket() 函数来创建的。因为服务器需要占用一个端口并等待客户端的请求,所以它们必须绑定到一个本地地址。特别地,TCP 服务器必须监听(传入)的连接。

  调用 accept() 方法之后,就开启了一个简单的(单线程)服务器,它会等待客户端的连接。默认情况下,accept() 是阻塞的,这意味着指定将被暂停,直到一个连接到达。一旦服务器接收一个连接,就会返回(利用 accept())一个独立的客户端套接字,用来与即将到来的消息交换。当一个传入的请求到达时,服务器会创建一个新的通信接口来直接与客户端进行通信,再次空出主要的端口,以使其接收新的客户端连接。

  一旦创建了临时套接字,通信就可以开始,通过使用这个新的套接字,客户端与服务器就可以开始参与发送和接收的对话中,直达连接终止。当一方关闭连接或者向对方发送一个空字符串时,通常就会关闭连接。

from socket import socket
from socket import AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR
from time import ctime

HOST = "127.0.0.1"
PORT = 8080
ADDRESS = (HOST, PORT)

tcp_server = socket(AF_INET, SOCK_STREAM)               # 创建服务器套接字
tcp_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)      # 解决端口占用问题
tcp_server.bind(ADDRESS)                                # 套接字与地址绑定
tcp_server.listen(5)                                    # 监听连接,最大挂起连接数为5

while True:                                             # 监听连接
    print("waiting for connection...")
    connection, address = tcp_server.accept()           # 接收客户端连接
    print("...connection from: ", address)

    while True:                                         # 通信循环
        try:
            recv_data = connection.recv(1024)           # 服务端接收消息,单次最大接收为1024个字节
            # 在Linux系统中,一旦data收到空,意味着是一种异常的行为:客户端非法断开连接
            if not recv_data:                           # 适用于Linux系统
                break
            print(f"收到客户端【{address}】发送的数据:{recv_data.decode('utf-8')}")
            connection.send(f"【{ctime()}】 {recv_data.decode('utf-8')}".encode("utf-8"))  # 服务端发送消息
        except ConnectionResetError:                    # 适用于Windows系统
            break

    connection.close()                                  # 关闭连接

tcp_server.close()                                      # 关闭服务器套接字

  在代码中,一个客户端连接关闭之后,服务器就会等到另一个客户端连接。

2.2、创建TCP客户端

  创建 TCP 服务器的伪代码如下:

import socket               # 导入socket模块

cs =  socket.socket()       # 创建客户端套接字
cs.connect()                # 尝试连接服务器

while True:                 # 通信循环
    cs.send()/cs.recv()     # 对话(发送/接收)

cs.close()                  # 关闭客户端套接字

  所有套接字都是利用 socket.socket() 创建的。然而,一旦客户端拥有了一个套接字,它就可以利用套接字的 connect() 方法直接创建一个到服务器的连接。当连接建立之后,它就可以直接参与到与服务器的一个对话中。最后,一旦客户端完成了它的事务,它就可以关闭套接字,终止此次连接。

from socket import socket
from socket import AF_INET, SOCK_STREAM

HOST = "127.0.0.1"
PORT = 8080
ADDRESS = (HOST, PORT)

tcp_client = socket(AF_INET, SOCK_STREAM)               # 创建客户端套接字
tcp_client.connect(ADDRESS)                             # 尝试连接服务器

while True:                                             # 通信循环
    send_data = input("请输入要发送的数据: ").strip()
    if not send_data:                                   # 按空格、回车等键结束连接
        break
    tcp_client.send(send_data.encode("utf-8"))          # 客户端发送数据,不允许发送空数据

    recv_data = tcp_client.recv(1024)                   # 客户端接收数据,单次最大接收为1024个字节
    if not recv_data:
        break
    print(f"收到服务端【{ADDRESS}】返回的数据:{recv_data.decode('utf-8')}")

tcp_client.close()                                      # 关闭客户端套接字

2.3、执行TCP服务器和客户端

  如果先运行客户端,那么将无法进行任何连接,因为没有服务器等待接受请求。服务器可以视为一个被动伙伴,因为必须首先建立自己,然后被动的等待连接。另一方面,客户端是一个主动的合作伙伴,因为它主动发起一个连接。换句话说,首先启动服务器(在任何客户端试图连接之前)。

  在开发中,创建这种 “友好的” 退出方式的一种方法就是,将服务器的 while 循环放在一个 try-except 语句中的 except 子句中,并监控 EOFError 或 KeyboardInterrupt 异常,这样你就可以在 except 或 finally 子句中关闭服务器的套接字。

三、粘包问题

3.1、什么是粘包

  TCP 是流式协议,数据向水流一样粘在一起,没有任何边界区分。如果接收数据没收干净,数据会有残留,就会和下一次的结果混淆在一起,就会出现粘包问题。粘包 指的是发送方发送的若干包数据到达接收方时粘成了一包,从接收缓冲区来看,后一包数据的头紧接着前一包数据的尾,出现粘包的原因是多方面的,可能是来自发送方,也可能是来自接收方。

  TCP 默认使用 Nagle 算法(主要作用:减少网络中报文段的数量),而 Nagle 算法主要做两件事:①只有上一个分组得到确认,才会发送下一个分组;②收集多个小分组,在一个确认到来时一起发送;因此,Nagle 算法造成了发送方可能会出现粘包问题。

  tcp_server.py 内容如下

from socket import socket
from socket import AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR

HOST = "127.0.0.1"
PORT = 8080
ADDRESS = (HOST, PORT)

tcp_server = socket(AF_INET, SOCK_STREAM)                           # 创建服务器套接字
tcp_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)                  # 解决端口占用问题
tcp_server.bind(ADDRESS)                                            # 套接字与地址绑定
tcp_server.listen(5)                                                # 监听连接,最大挂起连接数为5

print("waiting for connection...")
connection, address = tcp_server.accept()                           # 接收客户端连接
print("...connection from: ", address)

data = connection.recv(1024)                                        # 服务端接收消息,单次最大接收为1024个字节
print("收到客户端数据:%s" % data.decode("utf-8"))

connection.close()                                                  # 关闭连接
tcp_server.close()                                                  # 关闭服务器套接字

  tcp_client.py 内容如下:

from socket import socket
from socket import AF_INET, SOCK_STREAM

HOST = "127.0.0.1"
PORT = 8080
ADDRESS = (HOST, PORT)

tcp_client = socket(AF_INET, SOCK_STREAM)                   # 创建客户端套接字
tcp_client.connect(ADDRESS)                                 # 尝试连接服务器

tcp_client.send("hello ".encode("utf-8"))                   # 客户端发送数据
tcp_client.send("world!".encode("utf-8"))                   # 客户端发送数据

tcp_client.close()                                          # 关闭客户端套接字

3.2、TCP服务器

import subprocess
import struct
import json

from socket import socket
from socket import AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR

HOST = "127.0.0.1"
PORT = 8080
ADDRESS = (HOST, PORT)

tcp_server = socket(AF_INET, SOCK_STREAM)                           # 创建服务器套接字
tcp_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)                  # 解决端口占用问题
tcp_server.bind(ADDRESS)                                            # 套接字与地址绑定
tcp_server.listen(5)                                                # 监听连接,最大挂起连接数为5

while True:                                                         # 监听连接
    print("waiting for connection...")
    connection, address = tcp_server.accept()                       # 接收客户端连接
    print("...connection from: ", address)

    while True:                                                     # 通信循环
        try:
            # 1、接收客户端发送的命令
            cmd = connection.recv(1024)                             # 服务端接收命令,单次最大接收为1024个字节
            # 在Linux系统中,一旦data收到空,意味着是一种异常的行为:客户端非法断开连接
            if not cmd:                                             # 适用于Linux系统
                break

            # 2、执行系统命令,拿到结果
            obj = subprocess.Popen(cmd.decode("utf-8"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            stdout = obj.stdout.read()                              # 命令正确的返回结果
            stderr = obj.stderr.read()                              # 命令错误的返回结果

            # 3、把命令的结果返回给客户端
            # 第一步:制作固定长度的包头
            header_dict = {
                "total_size": len(stdout) + len(stderr)
            }

            header_json = json.dumps(header_dict)
            header_bytes = header_json.encode("utf-8")

            # 第二步:先发送报头的长度
            connection.send(struct.pack("i",len(header_bytes)))

            # 第三步:把报头(固定长度)发送给客户端
            connection.send(header_bytes)

            # 第四步:在发送真实的数据
            connection.send(stdout)
            connection.send(stderr)
        except ConnectionResetError:                                # 适用于Windows系统
            break

    connection.close()                                              # 连接

tcp_server.close()                                                  # 关闭服务器套接字

3.3、TCP客户端

import struct
import json

from socket import socket
from socket import AF_INET, SOCK_STREAM

HOST = "127.0.0.1"
PORT = 8080
ADDRESS = (HOST, PORT)


tcp_client = socket(AF_INET, SOCK_STREAM)                       # 创建客户端套接字
tcp_client.connect(ADDRESS)                                     # 尝试连接服务器

while True:                                                     # 通信循环
    # 1、向服务器发送命令
    data = input(">>>: ").strip()
    if not data:                                                # 按空格、回车等键结束连接
        break
    tcp_client.send(data.encode("utf-8"))                       # 客户端发送数据
  
    # 2、拿到命令的结果,并打印
    # 第一步:先接收报头的长度
    obj = tcp_client.recv(4)
    header_size = struct.unpack("i", obj)[0]

    # 第二步:再接收报头
    header_bytes = tcp_client.recv(header_size)

    # 第三步:从报头中解析出对真实数据的描述信息(数据的长度)
    header_json = header_bytes.decode("utf-8")
    header_dict = json.loads(header_json)
    total_size = header_dict["total_size"]

    # 第四步:接收真实的数据
    recv_size = 0
    recv_data = b""
    while recv_size < total_size:
        data = tcp_client.recv(1024)
        recv_data += data
        recv_size += len(data)

    # Windows默认的编码格式为gbk,Linux和MacOS默认的编码格式为utf-8
    print(recv_data.decode("gbk"))

tcp_client.close()                                               # 关闭客户端套接字

3.4、执行TCP服务器和客户端

  如果先运行客户端,那么将无法进行任何连接,因为没有服务器等待接受请求。服务器可以视为一个被动伙伴,因为必须首先建立自己,然后被动的等待连接。另一方面,客户端是一个主动的合作伙伴,因为它主动发起一个连接。换句话说,首先启动服务器(在任何客户端试图连接之前)。

  在开发中,创建这种 “友好的” 退出方式的一种方法就是,将服务器的 while 循环放在一个 try-except 语句中的 except 子句中,并监控 EOFError 或 KeyboardInterrupt 异常,这样你就可以在 except 或 finally 子句中关闭服务器的套接字。

四、并发TCP服务器

  我们使用线程的方式实现并发 TCP 服务器。

from socket import socket
from socket import AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR
from time import ctime
from threading import Thread

HOST = "127.0.0.1"
PORT = 8080
ADDRESS = (HOST, PORT)

tcp_server = socket(AF_INET, SOCK_STREAM)               # 创建服务器套接字
tcp_server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)      # 解决端口占用问题
tcp_server.bind(ADDRESS)                                # 套接字与地址绑定
tcp_server.listen(5)                                    # 监听连接,最大挂起连接数为5

class TCPServer(Thread):
    def run(self):
        print("waiting for connection...")

        while True:                                     # 监听连接
            connection, address = tcp_server.accept()   # 接收客户端连接
            print("...connection from: ", address)

            # 创建一个新的子线程,专门为刚刚创建的客户端服务
            handle_data_thread = HandleData(connection, address)
            handle_data_thread.start()

    def __del__(self):
        tcp_server.close()                              # 关闭服务器套接字

class HandleData(Thread):
    def __init__(self, connection, address):
        super().__init__()
        self.connection = connection
        self.address = address

    def run(self):
        while True:                                     # 通信循环
            try:
                recv_data = self.connection.recv(1024)  # 服务端接收消息,单次最大接收为1024个字节
                # 在Linux系统中,一旦data收到空,意味着是一种异常的行为:客户端非法断开连接
                if not recv_data:                       # 适用于Linux系统
                    break
                print(f"收到客户端【{self.address}】发送的数据:{recv_data.decode('utf-8')}")
                self.connection.send(f"【{ctime()}】 {recv_data.decode('utf-8')}".encode("utf-8"))  # 服务端发送消息
            except ConnectionResetError:                # 适用于Windows系统
                break

        print(f"客户端【{self.address}】断开连接!")
        self.connection.close()                         # 关闭连接

if __name__ == "__main__":
    server = TCPServer()
    server.start()

  tcp_client.py 内容如下:

from socket import socket
from socket import AF_INET, SOCK_STREAM

HOST = "127.0.0.1"
PORT = 8080
ADDRESS = (HOST, PORT)

tcp_client = socket(AF_INET, SOCK_STREAM)               # 创建客户端套接字
tcp_client.connect(ADDRESS)                             # 尝试连接服务器

while True:                                             # 通信循环
    send_data = input("请输入要发送的数据: ").strip()
    if not send_data:                                   # 按空格、回车等键结束连接
        break
    tcp_client.send(send_data.encode("utf-8"))          # 客户端发送数据,不允许发送空数据

    recv_data = tcp_client.recv(1024)                   # 客户端接收数据,单次最大接收为1024个字节
    if not recv_data:
        break
    print(f"收到服务端【{ADDRESS}】返回的数据:{recv_data.decode('utf-8')}")

tcp_client.close()                                      # 关闭客户端套接字
posted @ 2024-11-21 19:13  星光映梦  阅读(0)  评论(0编辑  收藏  举报