Python中TCP协议的理解

  • Post author:
  • Post category:python


Num01–>TCP通信模型

这里写图片描述

Test01–>TCP客户端案例

#! /usr/bin/env python3
# -*- coding:utf-8 -*- 

from socket import *

def main():

    # 1.创建socket
    client_socket = socket(AF_INET, SOCK_STREAM)

    # 2.指定服务器的地址和端口号
    server_addr = ('192.168.105.125',8080)
    client_socket.connect(server_addr)

    print('connect %s success' % str(server_addr))

    while True:
        # 3.给用户提示,让用户输入要检索的资料
        send_data = input('>>')
        # 退出
        if send_data == 'quit':
            break
        # 向服务器请求数据
        client_socket.send(send_data.encode())

    client_socket.close()

if __name__ == "__main__":
    main()

Test02–>TCP服务器端案例

TCP服务器端创建流程如下:

1,socket创建一个套接字

2,bind绑定ip和port

3,listen使套接字变为可以被动链接

4,accept等待客户端的链接

5,recv/send接收/发送数据

#! /usr/bin/env python3
# -*- coding:utf-8 -*- 

from socket import *
import time

def main():

    # 1.创建socket,stream流式套接字,对应tcp
    listen_socket = socket(AF_INET, SOCK_STREAM) 

    # 设置允许复用地址,当建立连接之后服务器先关闭,设置地址复用
    #  设置socket层属性    复用地址    允许
    listen_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)

    # 2.绑定端口号
    my_addr = ('192.168.105.125', 8080)
    #shift + insert
    listen_socket.bind(my_addr)

    # 3.接听状态
    #listen中的black表示已经建立连接的总数
    #如果当前已建立链接数以达到设定值,那么新客户端就不会connect成功,而是等待服务器。直到有链接退出。
    listen_socket.listen(4)
    print('listening...')

    # 4.等待客户端来请求服务器

    while True:

        # 接受连接请求,创建新的连接套接字,用于客户端连通信
        connect_socket, client_addr = listen_socket.accept() 
        # accept默认会引起阻塞 
        # 新创建连接用的socket, 客户端的地址
        # print(connect_socket)
        # print(client_addr)

        while True:
            # tcp recv() 只会返回接收到的数据
            # 1024表示接受的数据长度
            recv_data = connect_socket.recv(1024)

            if len(recv_data) == 0:
                #发送方关闭tcp的连接,recv()不会阻塞,而是直接返回''
                print('client %s close' % str(client_addr))

                time.sleep(5)
                break

            print('recv: %s' % recv_data.decode('gbk'))

        # 用完之后,关闭新创建的那个connect_socket
        connect_socket.close()

if __name__ == "__main__":
    main()

Num02–>TCP协议三次握手

这里写图片描述

Num03–>TCP协议四次挥手

这里写图片描述

x,,y都表示32位的随机数

SYN标志:表示发起连接请求

ACK标志:表示应答请求

seq发送方的顺序编号

ack应答方的应答编号=上一次seq编号+这次发送数据长度len

注意:数据长度len,在三次握手和四次挥手的过程中都为0,只在数据传输的时候,有实际的长度

在数据传输的时候,加以说明:

客户端发送SYN seq=x+1,假设数据长度len=10

服务器应答ACK ack=x+1+10

如果客户端再发送请求,那么就重复以上的两步SYN和ACK

FIN表示断开连接请求

FIN和SYN都会占用一个序列长度

问题一:为什么握手三次,而挥手四次?

原因就是三次握手的时候,在没有真正建立连接(三次握手结束)之前是不能发送应用数据的,服务器返回的时候,应答包和请求包是一个包。

而四次挥手是:当一个客户端主动发送断开close()请求时,服务器端却可以接收别的请求;要想真正的结束,那么服务器端就需要被动的调用断开close()。那么服务器返回的时候,ACK(应答包)和FIN(断开连接包)是不同的两个包。所以,需要四次挥手。

问题二:握手为什么是三次?两次行不行?为什么?

三次握手是为了安全,正确考虑,防止大部分的恶意攻击请求。

两次握手不行,因为如果有恶意的、不停的发请求,那么服务器每一次就认为是正确的请求;这样,由于服务器连接的请求数是一定的,那么这些恶意的请求会一直占服务器,会导致正常的用户连接不上服务器,长时间服务器就瘫痪了。

Num04–>TCP协议十种状态

这里写图片描述

当一端收到一个FIN,内核让read返回0来通知应用层另一端已经终止了向本端的数据传送

发送FIN通常是应用层对socket进行关闭的结果

Num05–>TCP协议的2MSL问题

这里写图片描述

加以说明:

1,2MSL即两倍的MSL,TCP的TIME_WAIT状态也称为2MSL等待状态。

2,当TCP的一端发起主动关闭,在发出最后一个ACK包后,

3,即第3次握 手完成后发送了第四次握手的ACK包后就进入了TIME_WAIT状态,

4,必须在此状态上停留两倍的MSL时间,

5,等待2MSL时间主要目的是怕最后一个 ACK包对方没收到,

6,那么对方在超时后将重发第三次握手的FIN包,

7,主动关闭端接到重发的FIN包后可以再发一个ACK应答包。

8,在TIME_WAIT状态 时两端的端口不能使用,要等到2MSL时间结束才可继续使用。

9,当连接处于2MSL等待阶段时任何迟到的报文段都将被丢弃。

10,不过在实际应用中可以通过设置 SO_REUSEADDR选项达到不必等待2MSL时间结束再使用此端口。

Num06–>TCP协议长链接和短链接

TCP在真正的读写操作之前,server与client之间必须建立一个连接,

当读写操作完成后,双方不再需要这个连接时它们可以释放这个连接,

连接的建立通过三次握手,释放则需要四次握手,

所以说每个连接的建立都是需要资源消耗和时间消耗的。

Test01–>长链接

1, client 向 server 发起连接

2,server 接到请求,双方建立连接

3,client 向 server 发送消息

4,server 回应 client

5,一次读写完成,连接不关闭

6,后续读写操作…

7,长时间操作之后client发起关闭请求

Test02–>短链接

1,client 向 server 发起连接请求

2,server 接到请求,双方建立连接

3,client 向 server 发送消息

4,server 回应 client

5,一次读写完成,此时双方任何一个都可以发起 close 操作

Test03–>长链接和短链接的区别

长链接可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间。

对于频繁请求资源的客户来说,较适用长连接。

client与server之间的连接如果一直不关闭的话,会存在一个问题,

随着客户端连接越来越多,server早晚有扛不住的时候,这时候server端需要采取一些策略,

如关闭一些长时间没有读写事件发生的连接,这样可以避免一些恶意连接导致server端服务受损;

如果条件再允许就可以以客户端机器为颗粒度,限制每个客户端的最大长连接数,这样可以完全避免某个蛋疼的客户端连累后端服务。

短链接对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段。但如果客户请求频繁,将在TCP的建立和关闭操作上浪费时间和带宽。

Test04–>TCP长/短链接的应用场景

长链接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。

每个TCP连接都需要三次握手,这需要时间,如果每个操作都是先连接,

再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,

再次处理时直接发送数据包就OK了,不用建立TCP连接。

例如:数据库的连接用长连接,如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。

像WEB网站的HTTP服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源。

像WEB网站这么频繁的成千上万甚至上亿客户端的连接,用短连接会更省一些资源;如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。虽然并发量大,但每个用户无需频繁操作情况下需用短连好。

Num07–>TCP并发服务器–多进程实现

通过为每个客户端创建一个进程的方式,能够同时为多个客户端进行服务。当客户端不是特别多的时候,这种方式还行,如果有几百上千个,就不可取了,因为每次创建进程等过程需要好较大的资源。

#! /usr/bin/env python3
# -*- coding:utf-8 -*-
# @Author  : xiaoke

from multiprocessing import Process
from socket import *


# 需要为客户端提供服务
def do_service(connect_socket):
    while True:
        recv_data = connect_socket.recv(1024)
        if len(recv_data) == 0:
            # 发送方关闭tcp的连接,recv()不会阻塞,而是直接返回''
            # print('client %s close' % str(client_addr))     
            # s.getpeername()   s.getsockname()
            print('client %s close' % str(connect_socket.getpeername()))
            break
        print('recv: %s' % recv_data.decode('gbk'))


def main():
    # 1.创建socket
    listen_socket = socket(AF_INET, SOCK_STREAM)
    # stream流式套接字,对应tcp

    # 设置允许复用地址,当建立连接之后服务器先关闭,设置地址复用
    # 设置socket层属性    复用地址,不用等2msl,    允许
    listen_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)

    # 2.绑定端口
    my_addr = ('192.168.105.125', 8080)
    listen_socket.bind(my_addr)

    # 3,接听状态
    listen_socket.listen(4)  # 设置套接字成监听,4表示一个己连接队列长度
    print('listening...')

    # 4.等待客户端来请求

    # 父进程只专注接受连接请求
    while True:
        # 接受连接请求,创建连接套接字,用于客户端间通信
        connect_socket, client_addr = listen_socket.accept()  # accept默认会引起阻塞
        # 新创建连接用的socket, 客户端的地址
        # print(connect_socket)
        print(client_addr)

        # 每当来新的客户端连接,创建子进程,由子进程和客户端通信
        process_do_service = Process(target=do_service, args=(connect_socket,))
        process_do_service.start()

        # 父进程,关闭connect_socket
        connect_socket.close()


if __name__ == "__main__":
    main()

Num08–>TCP并发服务器–多线程实现

#! /usr/bin/env python3
# -*- coding:utf-8 -*-
# @Author  : xiaoke
from socket import *
from threading import Thread


# 需要为客户端提供服务
def do_service(connect_socket):
    while True:
        recv_data = connect_socket.recv(1024)
        if len(recv_data) == 0:
            # 发送方关闭tcp的连接,recv()不会阻塞,而是直接返回''
            # print('client %s close' % str(client_addr))
            # s.getpeername()   s.getsockname()
            print('client %s close' % str(connect_socket.getpeername()))
            break
        print('recv: %s' % recv_data.decode('gbk'))


def main():
    # 1.创建socket
    listen_socket = socket(AF_INET, SOCK_STREAM)
    # 设置允许复用地址,当建立连接之后服务器先关闭,设置地址复用
    # 设置socket层属性    复用地址,不用等2msl    允许
    listen_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)

    # 2.绑定端口
    my_addr = ('192.168.105.125', 8080)
    # shift + insert
    listen_socket.bind(my_addr)

    # 3.接听状态
    listen_socket.listen(4)  # 设置套接字成监听,4表示一个己连接队列长度
    print('listening...')

    # 4.等待来电话

    # 主线程只专注接受连接请求
    while True:
        # 接受连接请求,创建连接套接字,用于客户端连通信
        connect_socket, client_addr = listen_socket.accept()  # accept默认会引起阻塞
        # 新创建连接用的socket, 客户端的地址
        # print(connect_socket)
        print(client_addr)

        # 每当来新的客户端连接,创建子线程,由子线程和客户端通信
        thread_do_service = Thread(target=do_service, args=(connect_socket,))
        thread_do_service.start()

        # 主线程,不能关闭connect_socket,多个线程共享打开的文件
        # connect_socket.close()


if __name__ == "__main__":
    main()

Num09–>TCP单进程阻塞服务器实现

#! /usr/bin/env python3
# -*- coding:utf-8 -*-
# @Author  : xiaoke
import time
from socket import *


def main():
    # 1.创建socket
    listen_socket = socket(AF_INET, SOCK_STREAM)

    # 设置允许复用地址,当建立连接之后服务器先关闭,设置地址复用
    #  设置socket层属性    复用地址    允许
    listen_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)

    # 2.绑定端口
    my_addr = ('192.168.105.125', 8080)
    listen_socket.bind(my_addr)

    # 3.接听状态
    listen_socket.listen(4)
    print('listening...')

    # 4.等待客户端发起请求
    while True:

        # 接受连接请求,创建连接套接字,用于客户端间通信
        connect_socket, client_addr = listen_socket.accept()  # accept默认会引起阻塞
        # 新创建连接用的socket, 客户端的地址
        # print(connect_socket)
        print(client_addr)

        while True:
            # tcp recv() 只会返回接收到的数据
            recv_data = connect_socket.recv(1024)
            if len(recv_data) == 0:
                # 发送方关闭tcp的连接,recv()不会阻塞,而是直接返回''
                print('client %s close' % str(client_addr))
                time.sleep(5)
                break
            print('recv: %s' % recv_data.decode('gbk'))
        # 用完之后,关闭connect_socket
        connect_socket.close()


if __name__ == "__main__":
    main()

Num10–>TCP单进程非阻塞服务器实现

#! /usr/bin/env python3
# -*- coding:utf-8 -*-
# @Author  : xiaoke
import time
from socket import *


def main():
    # 1.创建socket
    listen_socket = socket(AF_INET, SOCK_STREAM)
    # 设置允许复用地址,当建立连接之后服务器先关闭,设置地址复用
    # 设置socket层属性    复用地址    允许
    listen_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    # 设置listen_socket为非阻塞方式
    listen_socket.setblocking(False)  # 设置非阻塞

    # 2.绑定端口
    my_addr = ('192.168.105.125', 8080)
    listen_socket.bind(my_addr)

    # 3.接听状态
    listen_socket.listen(4)
    print('listening...')

    # 4.等待客户端发起请求
    # 创建一个列表,保存已连接socket
    connect_socket_list = []
    while True:
        print(connect_socket_list)
        try:
            # 接受连接请求,创建连接套接字,用于客户端连通信
            connect_socket, client_addr = listen_socket.accept()  # accept默认会引起阻塞 
        except Exception as e:  # 还没有客户端连接
            # print(e)
            # time.sleep(1)
            pass
        else:  # 此时有连接请求
            # 新创建连接用的socket, 客户端的地址
            print('有新的客户端连接 %s' % str(client_addr))
            # 将新socket 设成非阻塞
            connect_socket.setblocking(False)
            # 将新的socket添加到列表中,以便后续循环读数据
            connect_socket_list.append(connect_socket)
        # 保存删除socket列表
        need_delete_socket_list = []

        # 遍历已连接的socket分别读数据
        for new_socket in connect_socket_list:
            try:
                recv_data = new_socket.recv(1024)
            except:
                pass
            else:
                # 如果对方关闭
                if len(recv_data) == 0:
                    print('%s close' % (str(new_socket.getpeername())))
                    new_socket.close()
                    # 从connect_socket_list列表中删除,单独使用列表保存要删除socket
                    need_delete_socket_list.append(new_socket)
                    continue
                print('from %s : %s' %
                      (str(new_socket.getpeername()), recv_data.decode('gbk')))
        # 从connect_socket_list删除已关闭soccket
        for s in need_delete_socket_list:
            connect_socket_list.remove(s)
        time.sleep(1)


if __name__ == "__main__":
    main()

Num11–>IO多路复用–select版–TCP服务器实现

Test01–>select 原理

在多路复用的模型中,比较常用的有select模型和epoll模型。这两个都是系统接口,由操作系统提供。当然,Python的select模块进行了更高级的封装。

网络通信被Unix系统抽象为文件的读写,通常是一个设备,由设备驱动程序提供,驱动可以知道自身的数据是否可用。支持阻塞操作的设备驱动通常会实现一组自身的等待队列,如读/写等待队列用于支持上层(用户层)所需的block或non-block操作。设备的文件的资源如果可用(可读或者可写)则会通知进程,反之则会让进程睡眠,等到数据到来可用的时候,再唤醒进程。

这些设备的文件描述符被放在一个数组中,然后select调用的时候遍历这个数组,如果对于文件描述符可读则会返回该文件描述符。当遍历结束之后,如果仍然没有一个可用设备文件描述符,select则让用户进程睡眠,直到等待资源可用的时候再唤醒,唤醒之后遍历之前那个监视的数组。每次遍历都是依次进行判断的。

Test02–>select的优缺点

优点:select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。

缺点:select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。

一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.

对socket进行扫描时是依次扫描的,即采用轮询的方法,效率较低。

当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。

Test03–>案例的实现代码

#! /usr/bin/env python3
# -*- coding:utf-8 -*-
# @Author  : xiaoke
import select
import sys  # sys.stdin 代表键盘设备的文件对象
from socket import *


def main():
    # 1.创建socket
    listen_socket = socket(AF_INET, SOCK_STREAM)
    # 设置允许复用地址,当建立连接之后服务器先关闭,设置地址复用
    # 设置socket层属性    复用地址    允许
    listen_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)

    # 2.绑定端口
    my_addr = ('192.168.105.125', 8080)
    listen_socket.bind(my_addr)

    # 3.接听状态
    listen_socket.listen(4)
    print('listening...')

    # 指定select关心的哪些路(socket,或文件)数据
    rlist = [listen_socket, sys.stdin]  # 要读的文件对象列表,包括listen socket
    wlist = []  # 要写的文件对象列表
    xlist = []  # 出现异常的文件对象列表

    while True:
        print(rlist)
        # select 会阻塞等待三个列表中文件对象就绪,如果没就绪,select一直阻塞;只有任意文件就绪,select返回
        # 就绪的文件列表
        read_ready_list, wready, excplist = select.select(rlist, wlist, xlist)
        # 指定seclet关注读,写,异常文件列表

        # 如果select返回,一定有客户端连接服务器
        # 循环判断是哪个关注的文件,读就绪了
        for fobj in read_ready_list:
            # 如果fobj是listen_socket对象,一定有客户端连接服务器
            if fobj == listen_socket:
                new_socket, peer_addr = fobj.accept()  # 此时accept调用一定不会阻塞
                print(peer_addr)

                # 将新的socket添加至rlist,也要进行关注
                rlist.append(new_socket)

            elif fobj == sys.stdin:  # 键盘有数据输入

                data = sys.stdin.readline()  # input()

                print('input %s' % data)

                if data == 'quit\n':
                    exit()

            else:  # 已连接socket有数据可读
                recv_data = fobj.recv(1024)
                if len(recv_data) > 0:
                    print('from %s : %s' % (str(fobj.getpeername()), recv_data.decode('gbk')))
                else:  # 客户端关闭socket
                    print('%s close' % str(fobj.getpeername()))
                    fobj.close()  # 将关闭socket从rlist列表中删除,表示不再关注这个socket
                    rlist.remove(fobj)


if __name__ == "__main__":
    main()

Num12–>IO多路复用–epoll版–TCP服务器实现

Test01–>epoll的优点:

没有最大并发连接的限制,能打开的FD(指的是文件描述符,通俗的理解就是套接字对应的数字编号)的上限远大于1024

效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select和poll。

Test02–>一些术语

EPOLLIN (可读)

EPOLLOUT (可写)

EPOLLET (ET模式)

epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。

LT模式是默认模式,LT模式与ET模式的区别如下:

LT模式:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll时,会再次响应应用程序并通知此事件。

ET模式:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll时,不会再次响应应用程序并通知此事件。

Test03–>案例的实现代码

#! /usr/bin/env python3
# -*- coding:utf-8 -*-
# @Author  : xiaoke

import socket
import select

# 创建套接字
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 设置可以重复使用绑定的信息
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# 绑定本机信息
s.bind(("", 8080))

# 变为被动
s.listen(10)

# 创建一个epoll对象
epoll = select.epoll()

# 测试,用来打印套接字对应的文件描述符
# print s.fileno()
# print select.EPOLLIN|select.EPOLLET

# 注册事件到epoll中
# epoll.register(fd[, eventmask])
# 注意,如果fd已经注册过,则会发生异常
# 将创建的套接字添加到epoll的事件监听中
epoll.register(s.fileno(), select.EPOLLIN | select.EPOLLET)

connections = {}
addresses = {}

# 循环等待客户端的到来或者对方发送数据
while True:

    # epoll 进行 fd 扫描的地方 -- 未指定超时时间则为阻塞等待
    epoll_list = epoll.poll()

    # 对事件进行判断
    for fd, events in epoll_list:

        # print fd
        # print events

        # 如果是socket创建的套接字被激活
        if fd == s.fileno():
            conn, addr = s.accept()

            print('有新的客户端到来%s' % str(addr))

            # 将 conn 和 addr 信息分别保存起来
            connections[conn.fileno()] = conn
            addresses[conn.fileno()] = addr

            # 向 epoll 中注册 连接 socket 的 可读 事件
            epoll.register(conn.fileno(), select.EPOLLIN | select.EPOLLET)


        elif events == select.EPOLLIN:
            # 从激活 fd 上接收
            recvData = connections[fd].recv(1024)

            if len(recvData) > 0:
                print('recv:%s' % recvData)
            else:
                # 从 epoll 中移除该 连接 fd
                epoll.unregister(fd)

                # server 侧主动关闭该 连接 fd
                connections[fd].close()

                print("%s---offline---" % str(addresses[fd]))



版权声明:本文为u014745194原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。