Python网络编程笔记(二):TCP

IP层需要解决两个问题:

  1. 通过多路复用,区分不同应用程序的数据包
  2. 通过可靠传输,修复错误

两个主要协议:用户数据包协议(UDP)、传输控制协议(TCP),UDP用端口多路复用,解决问题一。TCP可以同时解决这两个问题。

TCP

TCP工作原理

  • 每个TCP数据包都有一个序列号,接收方通过序列号排序,和发现丢失数据包。
  • 序列号不是整数,使用计数器。新序列号 = 旧序列号 + 数据包长度
  • 初始序列号随机
  • 不锁步通信,无需等待响应一口气发送多个数据包。某一时刻发送方希望同时传输的最大数据量叫做TCP窗口(window)的大小
  • 接收方的TCP可以通过控制发送方的窗口大小来减缓或暂停连接,即流量控制(flow control)
  • 如果包被丢弃,TCP会假定网络很拥挤,会减少发送量。对于无线网这种噪声很大的环境来说,TCP是个灾难。

何时使用TCP

几乎所有情况下。

例外:

  1. 如果客户端只需向服务器发送单个较小请求,并且请求完成后无需通信,不需长连接,此时使用TCP大材小用了。
    • TCP建立连接至少三个包(SYN, SYN-ACK, ACK),结束连接三或四个包(FIN,FIN-ACK,ACK)。这样单个请求至少6个包。
    • 如果短时间内发送许多单独请求,可以用TCP。这时建立连接的开销可以被平均掉。
  2. 当丢包发生时,如果应用程序有比TCP重传更聪明的做法时。比如音频信号,一直重传丢失的1秒数据毫无必要。

TCP套接字的含义

TCP的connect()与UDP的connnect()不一样的:

  • TCP的是必须,建立连接;UDP的不必须,可以用sendto()指定地址。
  • TCP的connect()可能失败,因为真的在通信,建立连接。而UDP只是本机上记录下对方地址,并无通信。

套接字分类

  • 被动套接字/监听套接字:只用来接收连接请求,不发送和接收数据。
  • 主动套接字/连接套接字:只用于与特定一个远程主机通信,发送和接收数据。

一个被动套接字,可以有N个主动套接字。

Talk is Cheap, Show Me the Code

#!/usr/bin/env python3
# Foundations of Python Network Programming, Third Edition
# https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter03/tcp_sixteen.py
# Simple TCP client and server that send and receive 16 octets

import argparse, socket

def recvall(sock, length):
    data = b''
    while len(data) < length:
        more = sock.recv(length - len(data))
        if not more:
            raise EOFError('was expecting %d bytes but only received'
                           ' %d bytes before the socket closed'
                           % (length, len(data)))
        data += more
    return data

def server(interface, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    // 设置SO_REUSEADDR,确保套接字的端口在等待关闭状态时可以使用,否则OS需要几分钟后才使端口可用
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind((interface, port))
    // sock是监听套接字, listen的参数为等待连接的最大数。
    sock.listen(1)
    print('Listening at', sock.getsockname())
    while True:
        print('Waiting to accept a new connection')
        // 监听套接字sock调用accept()后新建了一个主动套接字sc
        sc, sockname = sock.accept()
        print('We have accepted a connection from', sockname)
        print('  Socket name:', sc.getsockname())
        print('  Socket peer:', sc.getpeername())
        message = recvall(sc, 16)
        print('  Incoming sixteen-octet message:', repr(message))
        sc.sendall(b'Farewell, client')
        sc.close()
        print('  Reply sent, socket closed')

// 由于TCP可靠性,客户端代码简单多了
def client(host, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((host, port))  // 相比于UDP,这里可能会失败
    print('Client has been assigned socket name', sock.getsockname())
    sock.sendall(b'Hi there, server')
    reply = recvall(sock, 16)
    print('The server said', repr(reply))
    sock.close()

if __name__ == '__main__':
    choices = {'client': client, 'server': server}
    parser = argparse.ArgumentParser(description='Send and receive over TCP')
    parser.add_argument('role', choices=choices, help='which role to play')
    // 绑定接口与UDP类似,区分自环接口127.0.0.1和空
    parser.add_argument('host', help='interface the server listens at;'
                        ' host the client sends to')
    parser.add_argument('-p', metavar='PORT', type=int, default=1060,
                        help='TCP port (default 1060)')
    args = parser.parse_args()
    function = choices[args.role]
    function(args.host, args.p)

send(), sendall()recv(), recvall()

TCP将数据看做流,会自行将流分割为多个数据包,因此可能发一半数据。而UDP的发送是原子性的,发送这个,接收这个。一次TCP send()时,操作系统网络栈会遇到三种情况:

  • 要发送数据立即被网络栈接收。send()立即返回,返回值是数据串长度。
  • 网卡正忙,发送缓冲区满,系统也不分配额外空间,send()会阻塞进程,暂停程序。
  • 介于两者,缓冲区几乎满了,但是还能放一点,放不全。则send()返回已经接收的长度。其余数据等待。

所以使用send()要循环判断其返回,比较麻烦,python提供了C写的sendall(),做这个循环。

recv()情况类似,如果接收缓冲区只有部分数据,也会立即返回。因此代码里有recvall(),保证一个循环内接收定长数据。标准库不提供,是因为用定长数据的太少了,而且很多数据需要边传边用,这种方案不具有普适性了。

死锁

上例是读取完整个数据流后再响应,缺点是如果流很大,内存可能就不够了。聪明点的做法时每次只读取并处理N字节的小型数据块。

考虑客户端大量数据要发送,客户端都来不及recv, 接收缓冲区就会被填满。此时服务器会探测到,然后阻塞send, 最终服务端发送缓冲区也满了,无法发送。服务器的接收缓冲区也会被填满,最终双方缓冲区都满,死锁。(即通信双方都忙着写数据,来不及读)

避免死锁:

  1. 将套接字选项的阻塞关闭。
  2. 使用某种技术同时处理来自多个输入的数据,比如多线程、多进程,select(), poll()等系统调用

UDP不会出现这种情况,接收不了就会丢弃。

已关闭连接,半关闭连接

套接字默认是双向的,shutdown()可以控制关闭任一方向。参数有三个:

  • SHUT_WR: 最常用,表示调用方将不再向套接字写入,而对方也不会再读取任何数据。
  • SHUT_RD: 关闭接收方向的数据流。
  • SHUT_RDWR: 关闭两个方向。与close()区别在于,close只关闭调用它的进程与套接字的关系,其他进程依旧可以用这个套接字。而这个就比较厉害了,全部关闭。

像使用文件一样使用TCP流

f = sock.makefile(), 然后就可以使用 read()write(), 底层会调用 recv()send()

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,039评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,223评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,916评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,009评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,030评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,011评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,934评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,754评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,202评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,433评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,590评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,321评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,917评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,568评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,738评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,583评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,482评论 2 352

推荐阅读更多精彩内容