一次python TCP socket编程引发的知识点
这次用python做一个tcp的服务器和客户端程序,主要用来做新建连接数测试。
1,新建连接数测试的原理
(1)首先tcp建立阶段,被测试设备需要转发3个TCP握手数据包;
(2)握手成功之后客户端会发送一个http GET请求给服务器;
(3)服务器收到GET请求之后会回复一个200 OK给客户端;
(4)客户端收到200 OK之后,就会发送一个rst报文断开当前连接;
(5)被测试设备收到rst报文就会删除当前tcp连接跟踪;
(6)服务端收到rst报文就会关闭当前tcp连接;
(7)重复上述步骤并在服务端统计收到的rst报文数量,以此记录一个完成的连接过程,统计单位时间内该数量就可以对被测试设备新建连接数进行衡量。
此处做的tcp测试程序主要的细节/问题处理在于如何发出rst报文,及如何在服务端统计每秒通过了多少连接数,涉及的python知识点有soket编程,全局变量,线程。
2,python TCP客户端程序
#python client.py
import socket
import struct
import sys
import thread
HOST=sys.argv[1]
PORT=sys.argv[2]
LOOP=sys.argv[3]
print(sys.argv[1], sys.argv[2], sys.argv[3])
def xinjian_test( threadName, threadLoop):
for i in range(1, int(threadLoop), 1):
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((HOST,int(PORT)))
#set reset attr
s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,struct.pack('ii', 1, 0))
#http get and recv 200 ok
s.sendall('Get.')
data=s.recv(1024)
#will send tcp reset
s.close()
try:
thread.start_new_thread( xinjian_test, ("Thread-1", LOOP, ) )
except:
print "Error: unable to start thread"
while 1:
pass
这里主要讲下tcp 在应用层如何发送rst报文:
(1)tcp发送rst报文的常规情况:
1,客户端尝试与服务器未对外提供服务的端口建立TCP连接,服务器将会直接向客户端发送reset报文(此reset报文为服务器主机内核tcp/ip协议栈发送,为tcp/ip协议栈机制)。
2,客户端和服务器的某一方在交互的过程中发生异常(如程序崩溃等),该方系统将向对端发送TCP reset报文,告之对方释放相关的TCP连接(可用ctrl+c模拟,可能在win和linux上表现不一样,参考:Ctrl+C在Linux平台和Windows平台下的TCP连接中的不同表现)
3,在交互的双方中的某一方长期未收到来自对方的确认报文,则其在超出一定的重传次数或时间后,会主动向对端发送reset报文释放该TCP连接(同样是内核协议栈机制)
4,应用开发者在设计应用系统时,会利用reset报文快速释放已经完成数据交互的TCP连接,以提高业务交互的效率(不用完成TCP四次挥手)
这次python的tcp客户端程序正是采用第4种情况来发送reset报文。
我们知道,通常情况,调用socket的关闭可以调用close或shutdown函数,这两个函数正常使用时,是按照tcp关闭连接的4次挥手过程进行的(他们的区别这里不做讨论),那么我们要发出rst包可能需要额外的处理,这里将要用到socket选项:
SO_LINGER套接口选项
A、l_onoff设置为0,这也是默认情况,函数close()是立即返回的,然后TCP连接双方是通过FIN、ACK4分组来终止TCP连接的。当然,发送缓冲区还有数据的话,系统将试着将这些数据发送到对方。
B、l_onoff非0,l_linger设置0,函数close()立即返回,并发送RST终止连接,发送缓冲区的数据丢弃。
C、l_onoff非0,l_linger非0,函数close()不立即返回,而是在
(a)发送缓冲区数据发送完并得到确认
(b)l_linger延迟时间到,l_linger时间单位为微妙。
两者之一成立时返回。如果在发送缓冲区数据发送完并被确认前延迟时间到的话,close返回EWOULDBLOCK(或EAGAIN)错误。
(2)python的tcp客户端将采用B方式发送rst报文:
#设置l_onoff非0,l_linger设置0
s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,struct.pack('ii', 1, 0))
#套接口关闭时,将发送rst报文,终止tcp连接
s.close()
2,python TCP服务端程序
#!/usr/bin/python3
#python3 main.py
import socketserver
import os,sys
import time
import threading
HOST1="192.168.16.10"
PORT1=8888
#这里省略HOSTn,PORTn定义(多个服务线程)
RST_SUM = 0
RST_TIME1 = int(time.time())
def calcu_pkt_rst(flag):
global RST_SUM
global RST_TIME1
RST_SUM += 1
RST_TIME2 = int(time.time())
RST_TIME3 = RST_TIME2 - RST_TIME1
if RST_TIME3 >= 1 :
print ("RST_SUM:", RST_SUM, "RST_TIME3:", RST_TIME3, "rst" if flag == True else "pkt", " of persecond:", RST_SUM/RST_TIME3)
RST_TIME1 = RST_TIME2
RST_SUM = 0
class Myserver(socketserver.BaseRequestHandler):
def handle(self):
conn = self.request
while True:
try:
#print("conn.recv. ")
ret_bytes = conn.recv(1024)
if not ret_bytes:
#print ("error.")
calcu_pkt_rst(False)
break
#print("ret_bytes ",ret_bytes)
except ConnectionResetError as e:
calcu_pkt_rst(True)
break
else:
conn.sendall(bytes("200 Ok.",encoding="utf-8"))
#print ("close.")
conn.close()
def xinjian_test( threadName, myhost, myport):
print ("host:", myhost, "port:", myport)
server = socketserver.ThreadingTCPServer((myhost,myport),Myserver)
server.serve_forever()
if __name__ == "__main__":
#这里省略tn(多个服务线程的初始化)
t1 = threading.Thread(target=xinjian_test, args=("Thread-1", HOST1, PORT1))
t1.start()
try:
t1.join()
except KeyboardInterrupt as e:
print ("KeyboardInterrupt: ", e)
pass
1)在服务端,通过套接口异常:ConnectionResetError来处理rst信息,这个过程是这样的:
a:服务端调用recv阻塞,等待客户端发送的信息
b:客户端连上服务器,并发送Get.信息,然后调用recv接收服务端返回的信息,此时线程将阻塞
c:服务端recv收到Get.信息,调用sendall发送200 Ok.信息,然后循环又回到a
d:客户端recv收到服务器的200 Ok.信息,将往下执行close,此时客户端将发送rst报文(此实际为内核协议栈发送)
e:服务器recv将扑获ConnectionResetError异常,因为服务器端的内核协议栈收到客户端的rst报文时,将会释放该tcp连接,而应用层recv此时还在等待该连接的信息,因此将触发异常
f:对该异常进行统计,到这里将是一个连接的完整来回,因此该统计可以表征中间被测设备的新建连接能力(当然前提是客户端和服务器端本身不是瓶劲)
2)采用python的全局变量机制进行统计,参考:『Python』 多线程 共享变量的实现
关键点在于:
对于一个全局变量,你的函数里如果只使用到了它的值,而没有对其赋值(指a = XXX这种写法)的话,就不需要声明global。相反,如果你对其赋了值的话,那么你就需要声明global。
声明global的话,就表示你是在向一个全局变量赋值,而不是在向一个局部变量赋值。
自己的体会:全局变量首先是应该全局声明的,如在服务端的程序开头就定义了全局变量:RST_SUM,在局部和函数体中需要对其赋值或改变其值时,需要显示使用global关键字进行声明,以表示他不是该函数体的局部变量,关于python的变量作用域,请参考:Python变量作用域及闭包
另外注意:不能在global声明语句进行赋值,如,global RST_NUM = 0
3)在程序的调试中碰到的异常:BrokenPipeError: [Errno 32] Broken pipe
关键信息:
File "main.py", line 68, in handle
conn.sendall(bytes("200 Ok.",encoding="utf-8"))
BrokenPipeError: [Errno 32] Broken pipe
我们看到,服务端在发送sendall的时候,出现了Broken pipe异常,通过抓包分析:
图1 图2其中,图1是产生Broken pipe异常的交互流,图2是无异常的交互流,我们看到在图1,在“此时应该是RST”报文处,发送了[FIN,ACK]报文,即192.168.1.230(客户端)告诉192.168.16.13(服务器端)这个TCP连接已经关闭,但是我们看到服务器端任然在该连接回[PSH,ACK],就是还在向该连接写数据,从tcp的四次挥手来讲,远端已经发送了FIN序号,告诉你我这个管道已经关闭,这时候,如果你继续往管道里写数据,第一次,你会收到一个远端发送的RST信号(我们看到接下来就是RST信号,这个信号不是客户端close触发的,是因为客户端发了[FIN,ACK]而服务器端任然在该连接回[PSH,ACK]),如果你继续往管道里write数据,操作系统就会给你发送SIGPIPE的信号,并且将errno置为Broken pipe(32)(这个继续写数据的数据包并没有出现在链路上被我们抓到),这是Broken pipe产生的原因。
那么,为什么客户端的close调用本应该产生的RST报文哪里去了?图1和图2的不同在于,客户端的运行环境,图2时在物理机上运行(win和Linux效果一样,我刚开始以为是Linux系统的问题),图1是在虚拟机上运行(Linux系统),不知道虚拟机的网络栈及接口为什么把我的RST报文变成了[FIN,ACK],而我们看到接下来虚拟机本身是能够发出RST报文的,这里的原因还没有进行深入分析。
另外,在调试这个问题的过程中,发现了另外一个问题:在服务端收到[FIN,ACK]到Broken pipe产生的过程中,python conn.recv(1024)一直不停的返回空字符串,也就是当python的TCP通道因为对方的[FIN,ACK]断开后,本来应该阻赛的recv一直收到空字符串,这就是为什么在服务器端会有这端代码的原因:
if not ret_bytes:
#print ("error.")
calcu_pkt_rst(False)
break
这段代码判断收到空串,则退出while循环,关闭该套接口。因此不会再走到sendall函数调用中去,这样不再触发Broken pipe错误,同时也可以完成程序设计的功能。
这里,有1个技术点澄清,还有一个疑问:
1)recv为什么收到空串:因为python的网络编程API是基于标准的 BSD Sockets API,可以访问底层操作系统Socket接口的全部方法。我们可以查看C语言recv的man page得到答案,其中:
RETURN VALUE
These calls return the number of bytes received, or -1 if an error occurred. In the event of an error, errno is set to indicate the error. The return value will be 0 when the peer has performed an orderly shutdown.
回应该篇文章:python socket.recv() 一直不停的返回空字符串,客户端怎么判断连接被断开?
2)从图1中可以看到客户端最后回了RST,为什么服务端程序没有响应到该异常,从程序代码执行顺序,按理recv先于sendall,为何感觉Broken pipe先于RST到来?