python的socket编程执行顺序_一次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异常,通过抓包分析:

40aedc3ed3b2

图1

40aedc3ed3b2

图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.

2)从图1中可以看到客户端最后回了RST,为什么服务端程序没有响应到该异常,从程序代码执行顺序,按理recv先于sendall,为何感觉Broken pipe先于RST到来?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值