美文网首页
一次python TCP socket编程引发的知识点

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

作者: 以前干嘛去了 | 来源:发表于2019-03-06 21:59 被阅读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到来?

    相关文章

      网友评论

          本文标题:一次python TCP socket编程引发的知识点

          本文链接:https://www.haomeiwen.com/subject/zwgxpqtx.html