一次python TCP socket编程引发的知识点

2019-03-06  本文已影响0人  以前干嘛去了

这次用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到来?

上一篇下一篇

猜你喜欢

热点阅读