目录


1 TCP Introduction

本节我们来介绍下TCP。TCP是Transmission Control Protocol(传输控制协议)的缩写。它和UDP一样,同属于Transport Layer里的协议。但是它可以在Network Layer上的不可靠的服务上建立可靠的服务。用三个关键字来概括TCP如下。

TCP:(IP+Port+Reliability).

2 TCP Programming

TCP相比于UDP,它的可靠建立在以下几个额外的基本功能:

  1. 3次握手建立连接和关闭连接:TCP传输数据前,需要先建立连接,通过3次握手(SYN,SYN-ACK,ACK);传输数据后,需要关闭连接(FIN,FIN-ACK,ACK)。需要注意的一点是如果传输1个packet,TCP的建立和关闭连接的cost就会很高,这个时候UDP更适合;如果是发送大量数据且持续时间较长的连接,TCP的建立和关闭连接的均摊成本就很低,这个时候TCP(额外的可靠性)要优于UDP。所以说TCP是重量级连接,UDP是轻量级连接。

  2. Flow Control:当receiver的socket buffer快满时,receiver会告诉sender发送慢一点,防止packet loss。这个发送量由TCP segment structure里的Receive Window决定。需要注意的是,如果sender每发送一个packet都得等待receiver接收到该packet后返回的ACK,才接着发送下一个packet,那么传输的效率就会很低下。TCP解决这个问题通过一次发送多个packet,然后根据receiver返回的ACK查看需不需要重发之前的packet。而这个同时发送多个packet的大小,就由Receive Window决定。

  3. Congestion ControlFlow Control只是针对receiver的单一sender而言,实际上,receiver会有多个sender。Congestoin Control就是receiver同时管制多个sender流量的方法。

  4. Fragmentation:TCP和UDP都会将一个大的数据在sender处碎片化和在receiver处重组。对于TCP,每一个segment都有一个编号,由TCP segment structure里的Sequence Number决定,记录该segment里的data的第1个byte在整个data中的位置,用以重组。TCP和UDP碎片化和重组的主要区别在于UDP的任何一个segment的丢失都会导致重组失败,而TCP在这种情况下只需要让sender重发一个丢失的segment即可。也就是说UDP的data是atomic的(要么接收,成功重组成整体;要么有丢失segment,不能成功重组成整体),TCP的data不是atomic(如果有丢失segment,那么sender请重发,直到receiver成功集齐所有segment,成功重组成整体;否则receiver一直固执地发送“请重发该sement”的请求,直到超时)。

Chapter 2 summary

下面我们来看下基本TCP的Client-Server代码。

2.1 基本TCP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import argparse,socket

def recvall(socket, length):
    data = b''
    while len(data) < length:
        more = socket.recv(length-len(data))
        if not more:
            raise EOFError("expected data length:{}; received data length: {}.".format(length,len(more)))
        data += more
    return data

def server(interface, port):
    sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1)
    sock.bind((interface,port))
    print('passive socket: {}'.format(sock.getsockname()))
    sock.listen(1)
    while True:
        conn, sockname = sock.accept()
        print('conn: {}'.format(repr(conn)))
        print('sock: {}'.format(repr(sock)))
        print('we\'have receive a connection from {}'.format(sockname))
        print('active socket:{}, client socket:{}'.format(conn.getsockname(),conn.getpeername()))
        data = recvall(conn, 16)
        print('client text: {}'.format(data.decode('utf-8')))
        text = data.decode('utf-8')
        data = text.encode('utf-8')
        conn.sendall(data)
        print('Farewell')
        conn.close()

def client(host, port):
    sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    sock.connect((host,port))
    print('client socket:{}'.format(sock.getsockname()))
    text = "Hi,there,goodman"
    data = text.encode('utf-8')
    sock.sendall(data)
    reply = recvall(sock,16)
    print('The server said {}'.format(reply.decode('utf-8')))
    sock.close()


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    choices = {"server":server,"client":client}
    parser.add_argument("role",choices=choices,help="chose role:server or client")
    parser.add_argument("host",help="the server's interface; client's host")
    parser.add_argument("-p",help="the port number",default="1060",type=int)
    args = parser.parse_args()
    function = choices[args.role]
    function(args.host,args.p)

2.1.1 Server Socket:Passive vs. Active

在服务端,有两种socket:

  1. passive socket:用于listen client的connect。在server端只有1个。

  2. active socket:是passive socket的accept()方法的第一个返回参数,用于数据传收。在server端可以有多个。

这是server 在TCP和UDP中最大的区别。socket是passive还是active由listen()决定,一旦调用listen(1),该socket就只能用于监听连接。其中参数1表示连接个数是1。

2.1.2 One Active Socket per Conversation

active socket在while里,表示可以建立多个active socket,每一个socket由(local_IP,local_Port,remote_IP,remote_Port)来作为ID,在这种情况下1个服务端可以有多个active socket对应不同的remote_IP和remote_Port来传收数据。active socket的getpeername()getsockname()可以分别获得socket两端的address(IP,Port)。

2.1.3 Sender发送多少长度

对于TCP而言,send(message)方法会有下面3种情况:

  1. Network Layer的sending buffer有足够空间可以完全接受message,这个时候send(message)返回len(message)表示已成功发送的数据长度(这里是全部);

  2. Network Layer的sending buffer半满,只能接受message的一部分数据,这个时候这个时候send(message)返回len(message_sent)表示已成功发送的数据长度(这里是部分);

  3. Network Layer的sending buffer已满,不能多接受1个byte,这个时候send(message)阻塞程序,直到sending buffer有空间可以接受bytes,即第1种或者第2种情况。

为了确保sender已经完全发送完message,有时候我们会看到如下代码。

1
2
3
4
5
byte_sent = 0

while byte_sent < len(message):
    message_remaining = message[byte_sent:]
    byte_sent = sock.send(message_remaining)

幸运的是Python的socket module已经用C帮我们实现了这个功能,封装在sendall()这个函数里。不仅sendall()比你自己实现要快,并且释放了全局解释器锁。

2.1.4 Receiver接收多少长度

对于recv()来说,情况是相似的,首先你得直到接受的数据长度length,然后有下面3种情况:

  1. Network Layer的receiving buffer有足够空间可以完全接收length长度的bytes,recv(length)返回成功接收的长度(这里是length);若是到达的数据长度小于length,则接收更没有问题。

  2. Network Layer的receiving buffer半满,可接收部分长度的bytes,recv(length)返回成功接收的数据(这里是message_part);

  3. Network Layer的sending buffer已满,不能多接受1个byte,这个时候recv(length)阻塞程序,直到receiving buffer有空间可以接受bytes,即第1种或者第2种情况。

我们将上面这3种逻辑封装在recvall(socekt,length)函数里。第二个参数是期望收到的数据总长度。

问题是Python为什么没有帮我们实现recvall()呢,很大原因是实际网络编程中,对于要接受的数据长度是不固定的。这需要留给更高级的protocol去解决。比如HTTP的response里Header line里就有一项Content-Length这一项,它就是告诉receiver将要收到多少bytes。但是需要注意的是你首先得实现接收bytes来督导Content-Length的数据,而这种工作,通常留给OS比较好。

在本例中,我们就用一个16 bytes的固定长度的数据给recvall()

2.1.5 Socket Option: SOL_SOCK,SO_REUSEADDR

可能细心的同学已经发现server在bind前设置了一些选项,sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1)

SOL_SOCK代表socket level是SOCK;SO_RESUEADDR代表SOCK option是REUSEQDDR;1代表设置为true。这行代码表示设置socket level里的resueaddr option为true。

这有什么好处呢?

如果注释掉这行代码,当你启动server和client,然后关掉server,重启server,你会发现“Address already in use” error。为什么会这样呢。这在于TCP关掉连接的时候通常需要来回好几次FIN,ACK。但是如何保障最后的ACK被收到呢,就好像A和B说:“我收到你的信息,我们现在关掉连接好吗?”;然后B对A说:”我收到你的信息,我们现在关掉连接好吗?“。如此往复却没有停止发送信息,实行关闭动作。通常的解决办法是当程序关掉server时,socket会被系统保留起来,处在一个CLOST-WAIT和TIME-WATI的状态,会保留4分钟确保双方都关闭后再释放。而sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1)就是确保处于系统保留list上的socket可以被复用而不用等到4分钟被释放后。

2.2 进阶TCP:死锁

死锁:当两个以上的运算单元,双方都在等待对方停止运行,以获取系统资源,但是没有一方提前退出时,这种状况,就称为死锁。

TCP程序很容易出现死锁。考虑一种情况,当client给server send 原始数据,并且在原始数据send完成后才开始read server send来的处理后数据;而server(即使片段化)处理client send来的原始数据并发送处理后的数据给client。这种情况下,当server send的数据量很大的时候,就会出现死锁,因为client 没send完原始数据,就read不了处理后的数据,这导致server的send也卡住,因为send不成功。通常IP protocol有read和send 4M buffer可以用来暂存数据。

上面这种情况用代码表示如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import argparse,socket,sys

def server(interface, port, bytecount):
    sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1)
    sock.bind((interface,port))
    print('passive socket: {}'.format(sock.getsockname()))
    sock.listen(1)
    while True:
        conn, sockname = sock.accept()
        byte_received = 0
        bytecount = (bytecount + 15) // 16 * 16
        while byte_received < bytecount:
            data = b''
            more = conn.recv(1024)
            if not more:
                break;
            data += more
            byte_received += len(more)
            print('server receive: {}; progress: {:.2f}%'.format(data.decode('utf-8'), byte_received/bytecount * 100))
            data = data.decode('utf-8').upper().encode('utf-8')
            conn.sendall(data)
            print('server send   : {}; progress: {:.2f}%'.format(data.decode('utf-8'), byte_received/bytecount * 100))
            sys.stdout.flush()
        print('Farewell')
        conn.close()

def client(host, port, bytecount):
    sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    sock.connect((host,port))
    bytecount = (bytecount+15)//16*16
    print('client is going to send {} bytes'.format(bytecount))
    byte_sent = 0
    text = 'Hello my friend '
    while byte_sent < bytecount:
        sock.sendall(text.encode('utf-8'))
        byte_sent += len(text)
        print('client send   : {}; progress: {:.2f}%'.format(text,byte_sent/bytecount*100))
        sys.stdout.flush()
    print('client send finished')
    sock.shutdown(socket.SHU_WR)

    byte_received = 0
    while byte_received < bytecount:
        reply = sock.recv(1024)
        if not reply:
            break
        byte_received += len(reply)
        print('client receive: {}; progress: {:.2f}%'.format(reply.decode('utf-8'),byte_received/bytecount*100))
        sys.stdout.flush()
    print('client receive finished')
    sock.close()


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    choices = {"server":server,"client":client}
    parser.add_argument("role",choices=choices,help="chose role:server or client")
    parser.add_argument("host",help="the server's interface; client's host")
    parser.add_argument("bytecount",help='the communicating bytenumber between sever and client',default=16,type=int)
    parser.add_argument("-p",help="the port number",default="1060",type=int)
    args = parser.parse_args()
    function = choices[args.role]
    function(args.host,args.p,args.bytecount)

若bytecount设置为1073741824(1G),就会出现死锁现象,server先卡住,然后client也卡住。解决的办法有两种:

  1. 设置socekt options,当send()recv()block的时候马上返回;

  2. 通过多线程或者运行系统的select()或poll()方法,来同时等待忙碌的发送或者接收的buffer,一旦某个有空就立即响应。

以上两种方法我们会在第7篇笔记里介绍。

2.2.1 Socket:关闭 vs. 半关闭

需要注意的是,我们用了一个常见的模式来表示socket的recv()到达了end-of-file,即返回的byte是空。

另外,TCP socket是双向的,如何将其设置为单向使得一方只能发送,另一方只能接收呢。也许你会问,单向的应用场景常见吗?答案是常见。上面这个例子中,即使大部分时间TCP socket是双向的,但是当client send结束时,就只需要改成单向,接收来自server的发送。那么如何改呢,用socket的shutdown()方法,它有3个参数:

  1. SHUT_WR:调用该方法的socket只能接受不能发送,peer只能发送,不能接收(得处理end-of-file情况)。

  2. SHUT_RD:调用该方法的socket只能发送不能接收,peer只能接收,不能发送。

  3. SHUT_RDWR:这个比较复杂。简单来说和socket的close()的区别在于该socket可能被多个process share,close()表示当前process不能使用,但是其他process可以使用;而shutdown(SHUT_RDWR)表示所有process都不能使用该socket。

同时将双向改成单向在只用单向的情况下更利于处理异常和错误。

2.2.2 Socket转换File

socket有makefile()方法可以创建file object。socket其实是特殊的一种file,也遵循打开open –> 读写write/read –> 关闭close的模式。但是python在socket和file之间做了明确的区分,file有read()write()方法;socket有recv()send()方法,如果想创建该soceket的file object,则要用到socket的makefile()

1
2
3
4
5
6
7
>>> import socket
>>> sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> hasattr(sock, 'read')
False
>>> f = sock.makefile()
>>> hasattr(f, 'read')
True

3 总结

TCP总结

4 参考资料


Share Post

Twitter Google+

Shunmian

The only programmers in a position to see all the differences in power between the various languages are those who understand the most powerful one.