0x00前言本文首先介绍了互联网的核心协议TCP,然后以Pythonsocket模块为例介绍了网络套接字。最后给出了TCP服务器端和客户端的Python脚本,并演示了两者之间的通信过程。0x01TCP协议TCP(TransmissionControlProtocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP协议的执行过程分为三个阶段:连接建立、数据传输和连接终止。其中“ConnectionEstablishment”和“ConnectionTermination”就是大家熟悉的TCP协议三次握手(TCPThree-wayHandshake)和TCP四次握手,也是理解TCP服务器和服务器通信过程的两个核心阶段本文中的客户端。为了更好的理解后面的过程,对TCP协议头的关键部分进行解释如下:报文的功能定义在TCP协议头的标志(Flags)部分,位于第104~111段位,共占用8位,每一位对应一个功能,设置1表示开启,设置0表示关闭。例如SYN报文的标识为00000010,ACK报文的标识为00010000,ACK+SYN报文的标识为00010010。报文的序号在TCP协议的SequenceNumber部分定义header,位于第32位到第63位,共占用32位。例如,在“三次握手”过程中,初始序列号seq是由数据发送方随机生成的。报文的确认号定义在TCP协议头的确认号(AcknowledgmentNumber)部分,位于第64位到第95位,共占用32位。例如,在“三次握手”过程中,确认号ack是上一个接收到的消息的序号加1,代表下一个预期接收到的消息的序号。连接创建所谓的“三次握手”,即TCP服务器与客户端成功建立通信连接所必需的三个步骤,需要通过三个消息来完成。一般来说,首先发送SYN报文的是客户端,服务器监听客户端的连接建立请求。握手步骤1客户端向服务器发送SYN报文(SYN=1),请求建立连接。此时消息的初始序列号为seq=x,确认号为ack=0。发送后,客户端进入SYN_SENT状态。握手步骤2服务端收到客户端的SYN报文后,发送ACK+SYN报文(ACK=1,SYN=1)确认客户端的连接建立请求,同时也向其发起连接建立请求。此时消息的序号为seq=y,确认号为ack=x+1。发送后,服务器进入SYN_RCVD状态。握手第三步:客户端收到服务器的SYN报文后,发送ACK报文(ACK=1)确认服务器的连接建立请求。此时消息的序号为seq=x+1,确认号为ack=y+1。发送后,客户端进入ESTABLISHED状态;服务器收到消息后,也进入ESTABLISHED状态。至此,“三次握手”过程全部结束,TCP通信连接成功建立。读者可以参考下面的“三次握手”示意图来理解:完全终止通讯连接,一共需要四次报告。正文完整。由于TCP通信连接是全双工的,每个方向的连接都可以单独关闭,可以看成是一对“秒波”,或者说是一对单工连接。主动发送FIN报文的一方先表示要关闭与对方的通信连接,即不再往这个方向发送数据,但对方发送的数据仍然可以接收到,直到对方收到一方也发送FIN报文,双方通信连接彻底终止。注意首先发送FIN报文的一方可以是客户端也可以是服务器。下面以客户端先发起关闭请求为例,来说明“挥手四次”的过程。握手步骤1当客户端不再向服务器发送数据时,发送FIN报文(FIN=1)请求关闭连接。此时消息的初始序列号为seq=u,确认号为ack=0,(如果本条消息中ACK=1,则ACK的值与客户端收到的上一条消息有关)。发送后,客户端进入FIN_WAIT_1状态。握手步骤2服务器收到客户端的FIN报文后,发送ACK报文(ACK=1)确认客户端关闭连接的请求。此时报文序号为seq=v,确认号为ack=u+1。发送后,服务器进入CLOSE_WAIT状态;当客户端收到消息后,进入FIN_WAIT_2状态。注意此时TCP通信连接处于半关闭状态,即客户端不再向服务端发送数据,但仍然可以接收到服务端发送的数据。握手步骤3当服务器不再向客户端传输数据时,发送FIN+ACK报文(FIN=1,ACK=1)请求关闭连接。此时消息的序号为seq=w(如果服务端在半关闭状态下还没有向客户端发送数据,则seq=v+1),确认号为ack=u+1.发送后,服务器进入LAST_ACK状态。握手步骤4客户端收到服务器的FIN+ACK报文后,发送ACK报文(ACK=1)确认服务器关闭连接的请求。此时消息的序号为seq=u+1,确认号为ack=w+1。发送完成后,客户端进入TIME_WAIT状态;服务器收到消息后,进入CLOSED状态;当客户端等待2MSL仍未收到服务器响应时,认为服务器已经正常关闭,进入CLOSED状态。至此,“四次挥手”过程全部结束,成功关闭TCP通信连接。读者可以参考下面的“四次挥手”示意图来理解:0x02NetworkSocketNetworkSocket(网络套接字)是计算机网络中进程间通信的数据流端点,广义上也代表了一个操作系统提供的进程间通信机制。进程间通信(IPC)的基本前提是能够唯一标识每个进程。在本地主机的进程间通信中,可以使用PID(进程ID)来唯一标识每个进程,但PID只是本地唯一的,网络中不同主机的PID可能会发生冲突,所以“IP地址+传输层协议+端口号”来唯一标识网络中的一个进程。Tips:网络层的IP地址可以唯一标识主机,传输层的TCP/UDP协议和端口号可以唯一标识主机的一个进程。注意TCP协议和UDP协议在同一台主机上可以使用相同的端口号。所有支持网络通信的编程语言都提供了一套socketAPI。下面以Python3为例,讲解一下服务端与客户端建立TCP通信连接的交互过程:对上述过程有了一定印象后,就更容易理解了下面两节分别实现Python的实现TCP服务器和客户端。0x03TCPserver#!/usr/bin/envpython3#-*-coding:utf-8-*-#公众号:python学习开发importsocketimportthreadingdeftcplink(conn,addr):print("Acceptnewconnectionfrom%s:%s"%addr)conn.send(b"Welcome!\n")whileTrue:conn.send(b"What'syourname?")data=conn.recv(1024)ifdata==b"exit":conn.send(b"再见!\n")breakconn.send(b"Hello%s!\n"%data)conn.close()print("Connectionfrom%s:%sisclosed"%addr)s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)s.bind(("127.0.0.1",6000))s.listen(5)print("Waitingforconnection...")whileTrue:conn,addr=s.accept()t=threading.Thread(target=tcplink,args=(conn,addr))t.start()第6行:定义一个tcplink()函数,第一个conn参数是server和client之间进行数据交换的socket对象,第二个addr参数是client的IP地址和端口号由二元组(主机、端口)表示。第8行:连接成功后,向客户端发送欢迎信息b"Welcome!\n"。第9行:进入与客户端数据交互的循环阶段。第10行:发送查询消息b“What'syourname?”给客户。第11行:接收客户端发送的字节对象。第12行:如果bytes对象为b“exit”,则向客户端发送结束响应消息b“再见!\n”,结束与客户端的数据交互循环阶段。第15行:如果bytes对象不是b"exit",则向客户端发送问候响应消息b"Hello%s!\n",其中%s是客户端发送的bytes对象。第16行:关闭套接字,不再向客户端发送数据。第19行:创建一个socket对象,第一个参数是socket.AF_INET,代表使用IPv4协议进行网络通信,第二个参数是socket.SOCK_STREAM,代表使用TCP协议进行面向连接的网络通信。第20行:将服务器主机地址(“127.0.0.1”,6000)绑定到socket对象,即本地主机的TCP端口6000。第21行:开启socket对象的监听功能,等待客户端的连接请求。第24行:进入监听客户端连接请求的循环阶段。第25行:接收客户端的连接请求,获取与客户端交互的socket对象conn和客户端的IP地址和端口号addr,其中addr为二元组(host,port)。第26行:利用多线程技术,为每个请求连接的TCP客户端创建一个新的线程,实现一个服务器同时与多个客户端通信的功能。第27行:启动新线程的活动。0x04TCPclient#!/usr/bin/envpython3#-*-coding:utf-8-*-#公众号:python学习开发importsockets=socket.socket(socket.AF_INET,socket.SOCK_STREAM)s.connect(("127.0.0.1",6000))print(s.recv(1024).decode())data="client"whileTrue:ifdata:print(s.recv(1024).decode())data=input("Pleaseinputyourname:")ifnotdata:continues.send(data.encode())print(s.recv(1024).decode())ifdata=="exit":breaks.close()第5行:创建socket对象,首先第一个参数是socket.AF_INET,代表使用IPv4协议进行网络通信,第二个参数是socket.SOCK_STREAM,代表使用TCP协议进行面向连接的网络通信。第6行:向("127.0.0.1",6000)主机发起连接请求,即本地主机的TCP端口6000。第7行:连接成功后,收到服务器的欢迎信息b"Welcome!\n",转换成字符串打印出来。第9行:创建一个非空字符串变量data,并将初始值赋给“client”(只要是非空字符串即可),用于判断是否收到查询消息b“What'syourname?”从服务器。第10行:进入与服务器交换数据的循环阶段。第11行:当变量数据不为空时,接收到服务器的查询报文。第13行:要求用户输入名称。第14行:当用户输入为空时,循环重新开始,要求用户再次输入。第16行:当用户输入不为空时,将字符串转换为bytes对象发送给服务器。第17行:接收服务器的响应数据,将响应字节对象转换成字符串打印出来。第18行:当用户输入“exit”时,结束与服务器交换数据的循环阶段,即将关闭socket。第21行:关闭套接字,不再向服务器发送数据。0x05TCP进程间通信TCPserver和client的脚本分别命名为tcp_server.py和tcp_client.py,然后保存到桌面。笔者将在Windows10系统下使用PowerShell进行演示。Tips:读者复现时,请确保机器上安装了Python3。注意,作者将默认的启动路径名python修改为python3。单服务器VS单客户端在其中一个PowerShell中运行命令python3./tcp_server.py,服务器显示Waitingforconnection...,并监听本地主机的TCP端口6000,进入等待连接状态;在另一个PowerShellpython3./tcp_client.py中运行命令,服务器显示Acceptnewconnectionfrom127.0.0.1:42101,完成与本地主机TCP42101端口的通信连接建立,并发送欢迎信息和查询信息给客户端,客户端收到信息后打印出来;如果客户端向服务器发送字符串Alice和Bob,会收到服务器的问候响应;如果客户端向服务器发送空字符串,将要求重新输入;如果客户端向服务器发送字符串exit,它会收到给服务器的结束响应消息;客户端与服务器的通信连接关闭,服务器显示Connectionfrom127.0.0.1:42101isclosed,继续监听客户端的连接请求。单服务器VS多客户端在其中一个PowerShell中运行命令python3./tcp_server.py,服务器显示Waitingforconnection...,并监听本地主机的TCP端口6000,进入等待状态;在另外三个PowerShell中分别运行命令python3./tcp_client.py,服务器同时与本地主机的TCP端口42719、42721、42722建立通信连接,并发送欢迎信息和查询信息到client分别,client收到信息后打印出来;三个客户端Client分别向服务器发送字符串Client1、Client2、Client3,并接收服务器的问候响应信息;所有客户端分别向服务端发送exit字符串,并接收到服务端的结束响应信息;所有client-server通信连接关闭,server继续监听client的连接请求。0x06PythonAPIReferencesocket模块本节介绍上述代码中使用的内置模块socket,它是Python网络编程的核心模块。socket()函数socket()函数用于在网络通信中创建套接字对象。函数原型如下:socket.socket(family=AF_INET,type=SOCK_STREAM,proto=0,fileno=None)family参数表示地址族(AddressFamily),默认值为AF_INET,用于IPv4网络通信,常用AF_INET6,用于IPv6网络通信。family参数的可选值取决于本机操作系统。type参数表示套接字的类型。默认值为SOCK_STREAM,用于TCP协议(面向连接)网络通信,SOCK_DGRAM常用于UDP协议(无连接)网络通信。proto参数代表socket的协议,默认值为0,一般忽略这个参数,除非family参数是AF_CAN,那么proto参数需要设置为CAN_RAW或CAN_BCM。fileno参数表示socket的文件描述符,默认值为None。如果设置了这个参数,其他三个参数将被忽略。socket对象创建后,需要使用对象的内置函数来完成网络通信过程。注意下面函数原型中的“socket”指的是socket对象,不是上面说的socket模块。bind()函数bind()函数用于将IP地址和端口号绑定到套接字对象。注意socket对象一定不能绑定,端口号也不能被占用,否则会报错。函数原型如下:socket.bind(address)address参数表示要绑定到socket的地址,其格式取决于socket的family参数。如果family参数是AF_INET,address参数表示为二元组(host,port),其中host是用字符串表示的主机地址,port是用整数表示的端口号。listen()函数listen()函数用于TCP服务器开启socket监听功能。函数原型如下:socket.listen([backlog])backlog可选参数表示在socket拒绝新连接之前操作系统可以挂起的最大连接数。backlog参数一般设置为5,如果不设置,系统会自动为其设置一个合理的值。connect()函数connect()函数用于TCP客户端向TCP服务器发起连接请求。函数原型如下:socket.accept()address参数表示要连接到socket的地址,其格式取决于socket的family参数。如果family参数是AF_INET,address参数表示为二元组(host,port),其中host是用字符串表示的主机地址,port是用整数表示的端口号。accept()函数accept()函数被TCP服务器用来接受来自TCP客户端的连接请求。函数原型如下:socket.accept()accept()函数的返回值是一个二元组(conn,address),其中conn是服务端用来与客户端交换数据的socket对象,而address是客户端的IP地址和端口号,用二元组(主机,端口)表示。send()函数send()函数用于向远程套接字对象发送数据。注意在使用该函数之前,本地socket必须成功连接到远程socket,否则会报错。可见send()函数只能用于TCP进程间通信,而sendto()函数应该用于UDP进程间通信。函数原型如下:socket.send(bytes[,flags])bytes参数表示要发送的bytes对象数据。例如,对于字符串“helloworld!”,需要使用encode()函数将其转换为bytes对象b“helloworld!”。用于网络传输。flags可选参数用于设置send()函数的特殊功能,默认值为0,也可以由一个或多个预定义值组成,以位或运算符|分隔。详见Unix函数手册中的send(2)。flags参数的常用值包括MSG_OOB、MSG_EOR、MSG_DONTROUTE等。send()函数的返回值是发送数据的字节数。recv()函数recv()函数用于从远程套接字对象接收数据。请注意,与send()函数不同,recv()函数可用于TCP进程间通信和UDP进程间通信。函数原型如下:socket.recv(bufsize[,flags])bufsize参数表示socket可以接收的最大数据字节数。注意,为了更好的匹配硬件设备和网络传输,bufsize参数的值最好设置为2的幂,比如4096。flags可选参数用于设置recv()的特殊函数函数,默认值为0,也可以由一个或多个预定义值组成,以位或运算符|分隔。具体请参考Unix函数手册中的recv(2)。flags参数的常见值包括MSG_OOB、MSG_PEEK、MSG_WAITALL等。recv()函数的返回值是接收到的字节对象数据。例如,如果字节对象b"helloworld!"收到后,最好使用decode()函数将其转换成字符串“helloworld!”然后打印出来。close()函数close()函数用于关闭本地套接字对象并释放连接到该套接字的所有资源。socket.close()threadingmodule本节介绍上述代码中使用的内置模块threading,它是Python多线程的核心模块。Thread()类Thread()类可以创建一个线程对象,该对象用于调用start()函数来启动一个新的线程。类原型如下:classthreading.Thread(group=None,target=None,name=None,args=(),kwargs={},*,daemon=None)group参数是实现ThreadGroup的保留参数()类在未来。目前默认值为无。target参数表示线程被run()函数激活后调用的函数。默认值为None,即不会调用任何函数。name参数表示线程名称。默认值为无,系统会自动命名。格式为“Thread-N”,其中N为从1开始的十进制数。args参数表示target参数指向的函数的普通参数,用元组(tuple)表示,默认值为一个空元组()。kwargs参数表示target参数指向的函数的关键字参数,用字典(dict)表示,默认值为空字典{}。daemon参数用来表示进程是否是守护进程。如果设置为True,它会被标记为守护进程;如果设置为False,则会被标记为非守护进程;如果设置为None,则会继承当前父线程的daemon参数值。创建线程对象后,需要使用该对象的内置函数来控制多线程活动。start()函数start()函数用于启动线程活动。函数原型如下:Thread.start()注意每个线程对象只能调用一次start()函数,否则会引发RuntimeError错误。0x07总结本文介绍了TCP协议和socket编程的基础知识,然后用Python3实现并演示了TCP服务端和客户端的通信过程,同样使用了简单的多线程技术,最后让PythonAPI参与其中script成为一个参考索引,帮助理解执行过程。
