通用技术 用 Python 写网络编程(二)

陈子昂 · 2021年02月06日 · 最后由 Vence 回复于 2021年04月26日 · 11102 次阅读
本帖已被设为精华帖!

回溯前文

第一篇只是开篇了数据结构和一些传输的例子。第二篇会讲网络编程传输协议在 Python 场景下如何使用。
根据上文的回复,顺序上会先讲 Tcp,下面进入正题。

深度优化过的传输协议-Tcp

Tcp 标准来自上个世纪 80 年代,也是历经了 30 多年的改进和优化。但是这些优化不是应用层的,避免长篇大论,有兴趣可以自己去了解下。
这里面一些观点后面也会用 Python 去验证。
那么学习网络协议,在第一篇里面是协议用的数据结构。下一步就是学习 Tcp 和常规的 http 镞有什么不同,先从结构上进行梳理,结构分别有:

  1. 传输形态(包含传输的各种设置)
  2. 链接地址

传输形态

Http 在应用层分为多个域,整体比较复杂,比 TCP 更复杂,只是 Http 使用场景比 Tcp 广域,所以熟能生巧和更多理解。
Http 镞和 Tcp 底层都是 socket,这个 http 在 web 那层做了大量限制和改造,所以有了更多域。
Http 镞不打算这里介绍,进入正题。Tcp 也是数据流,可以获得字节数组 bytes 长度的。
传输形态这层是由 socket 做的,Python 那层做了大量的包容,不用设置很复杂的 socket 设置。

socket 原生函数需要通过 nodename、servname、ai_flags、ai_family、ai_socktype、ai_protocol 来设置
ai_family:AF_INET Ipv4,目前大部分都是 Ipv4。 AF_INET6,就是 Ipv6。
ai_socktype:SOCK_STREAM 是数据流,也就是 TCP 的流设置。SOCK_DGRAM 是数据包,UDP 的包设置。

def tcp_client_options():
    """
    tcp客户端设置的IPv4和数据流 (使用者)
    :return:
    """
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #通道设置
    return sock

def tcp_server_options():
    """
    tcp服务器设置(管理者)
    :return:
    """
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    """
    level=socket.SOL_SOCKET 所在哪个协议层
    socket.SO_REUSEADDR 访问选项名 这个是一个参数int类型
    """
    # SO_REUSEADDR端口释放后立即就可以被再次使用 1代表开启就可以使用
    sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    print(sock.getsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR))

服务器对比客户端来说是管理者身份,管理客户端链接,所以会多一个 setsockopt,设置 socket 规则或者叫配置。而客户端是使用者,所以只有设置通道(访问目标通道方式的)。

这个有个前置知识是 Tcp 握手和挥手,也是重要面试题,这里不做描述。
socket.SOL_SOCKET 是默认的代表你选择的协议层,也就是 socket 都是这个。
socket.SO_REUSEADDR 是设置,TCP 挥手后还能被使用,只能设置 1 和 0。

sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) 

这里修改为 2 或者-1 下面打印还是 1,这个也是 Python 做了大量的封装包容的体现。
socket.SO_REUSEADDR 代表的是 optname 的控制方式,也就是挥手断开连接。
Http 是单次请求交互模式,也就是说你发一个数据流请求,会立即被投递到服务器,进行验证合法,合法后应答后
Tcp 不是,Tcp 是发一组(多段数据流请求后),但是服务器不是立即接受,是会拥堵在网络缓存区内,到达一定数量在发给服务器。
所以这个也是初学者经常会遇到的,假设服务器完全接收到客户端信息,客户端发了 10 次大小共 5000 个字节,服务器那边往往只会收到 3-4 次,大小总量也是 5000 个字节。这个可以在看完本文后,自己试试写个例子。

可以设置缓存区大小,缓存区大小会和 Tcp 重要概念有关,前面例子 plus:

def tcp_server_options():
    """
    tcp服务器设置
    :return:
    """
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    print(sock.getsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR))
    #设置发送缓存区容纳单位
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 16 * 1024)
    print(f'Send -> {sock.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)}')
    print(f'Revice -> {sock.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF)}')

socket.SO_RCVBUF 是由协议层决定的,协议层这里常量是通过协议层默认大小来表示(双关),65535+1 的 65536,当然这个默认大小可以修改的,通常建议修改成 10 万,根据机器因地制宜。
socket.SO_SNDBUF 就是 16 * 1024 的,那么如果不设置会是多少呢。在 socket 层有很多双关,默认也是 65536。
还可以设置通信阻塞,这个阻塞是服务器处理客户端 socket 请求的,阻塞是会处理完才会接收同一个客户端的下一条请求,但是高并发场景下,多个 work 线程的 Tcp 服务器一般都是非阻塞的,这里的设置是全局设置的,会对整个服务器当前启动生效。

sock.setblocking(False) #设置非阻塞的。

还可以设置超时时间,但是不要画鱼加红烧肉这种做法,添加了超时时间会和上面设置阻塞有一些冲突,会影响正常设置了缓存区大小等,所以不推荐使用。
这个和平时测试工具使用的 requests 里面 timeout 不一样。
常规设置都已经讲了,下面讲服务器如何去让客户端投递数据给他。

2.链接地址
根本上没差别,网络传输从 1 端到另外 1 端,要传输都需要有地址 接收者 iP+ 端口。
客户端需要知道服务器的链接地址才能把消息投递到正确的地方,服务器拥有接收者 iP 会分配一个地址也就是端口,然后 bind 端口,监听链接到这个地址去管理客户端。

服务器还会有一个最大支持链接数,客户端是没有这个设置的,下面例子里面 max=5000,所以客户端和服务器关系是多对一的,最多支持 5000 个客户端链接到同个服务器。
看到上面,划重点关键字 bind 和 listen,给个例子,区分客户端和服务器代码:

def server_listener(addr: tuple, max: int):
    """
    服务器监听 最大支持5000个链接数
    :param max:最大支持链接数
    :param addr:元组
    :return:
    """
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 设置挥手后可以使用
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(addr)
    # 最大支持max个链接数
    sock.listen(max)
    return sock

if __name__ == '__main__':
    server_listener(("0.0.0.0", 12580),5000)

http 的服务器因为包装的很好,并不会有这种设置,服务器都是一个监听客户端 fd 和轮询管理这些 fd 的。
客户端&服务器用同一套 socket options,这样服务器才能接收链接客户端的消息,fd=sock.fileno()。

获取方式以及客户端链接代码,如下例子:

def create_client():
    """
    创建客户端
    :return:
    """
    # 客户端和服务器设置通道的socket肯定是要一样的,否则链接不过来
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sockfd = sock.fileno()
    print(f"客户端fd={sockfd}")
    sock.connect(("127.0.0.1", 12580))
    return sock
if __name__ == '__main__':
    client = create_client()
    print(client)

客户端链接代码里面,地址不能是 0.0.0.1,本机测试也只能是 127.0.0.1。
connect_ex() 不为 0,会返回 error 的码,会在和协议号和错误协议号串联一起的后面章节来讲。

服务器监听客户端 fd 的代码如下,核心函数是 accept():

def accept_client_connect(listener_sock):
    """
    服务器处理 确认客户端链接 核心方法是accept()
    :param listener_sock:服务器sock对象
    :return:
    """
    new_conn = listener_sock.accept()
    if new_conn == None:
        return
    sock, addr = new_conn
    # 服务器监听可以拿到那边拿sock的fd
    print(f"---accept_client_connection, sockfd={sock.fileno()}, addr={addr}")
    return sock

if __name__ == '__main__':
    sock = server_listener(("0.0.0.0", 12580), 5000)
    sock_ = accept_client_connect(sock)
    print(sock_)

最终打印

---accept_client_connection, sockfd=512, addr=('127.0.0.1', 14024)
<socket.socket fd=512, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 12580), raddr=('127.0.0.1', 14024)>

addr 端口就是服务器给客户端临时分配的端口,服务器就是通过字典管理,sockfd 和 addr[1],下面这段描述可以让其他语言也可以理解。

ClientInfo = {"client_fd":sockfd,"fd_port":addr[1]}
ClientGroup = []ClientInfo   
{"gateway_threads":ClientGroup} 

Workthread 分属不同作用,服务器里面的 gateway 服务 bind clientGroup 数组,这些细节后面章节会有具体的例子。

预告&总结

预告 socket 发包部分流程
step1: socket options(第二篇)
step2: 断开 socket,struct+pack 包 (数据结构)+ 压包 (数据结构在第一篇,第三篇是断开 socket 和 struct)
step3: 压缩的多种形式 (第四篇)
step4: 加密的多种形式 (第五篇)

总结

  1. 需要对 socket 设置做大量练习和理解更深入的。其实 http 非 Python 得也有很多设置,建议也可以深入理解。
  2. 写法上都会支持客户端,服务器例子都有。
  3. 没有讲的 socket.shutdown 和 struct 这个库可以预习下。
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 3 条回复 时间 点赞

大猫这一个系列,文笔进步很大。

恒温 将本帖设为了精华贴 02月07日 10:33
恒温 回复

赞同😊

陈子昂 用 Python 写网络编程(三) 中提及了此贴 02月14日 21:28
陈子昂 用 Python 写网络编程(三) 中提及了此贴 02月14日 21:28
陈子昂 用 Python 写网络编程(三) 中提及了此贴 02月14日 21:28

我写了个 tcp 服务器,在 red hat 下,关闭了服务器,但是 客户端那边没有断开,在 winodws 下是 OK 的

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册