目录
1 TCP Introduction
本节我们来介绍下TCP。TCP是Transmission Control Protocol(传输控制协议)的缩写。它和UDP一样,同属于Transport Layer里的协议。但是它可以在Network Layer上的不可靠的服务上建立可靠的服务。用三个关键字来概括TCP如下。
TCP:(IP+Port+Reliability).
2 TCP Programming
TCP相比于UDP,它的可靠建立在以下几个额外的基本功能:
-
3次握手建立连接和关闭连接:TCP传输数据前,需要先建立连接,通过3次握手(SYN,SYN-ACK,ACK);传输数据后,需要关闭连接(FIN,FIN-ACK,ACK)。需要注意的一点是如果传输1个packet,TCP的建立和关闭连接的cost就会很高,这个时候UDP更适合;如果是发送大量数据且持续时间较长的连接,TCP的建立和关闭连接的均摊成本就很低,这个时候TCP(额外的可靠性)要优于UDP。所以说TCP是重量级连接,UDP是轻量级连接。
-
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决定。
-
Congestion Control:Flow Control只是针对receiver的单一sender而言,实际上,receiver会有多个sender。Congestoin Control就是receiver同时管制多个sender流量的方法。
-
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”的请求,直到超时)。
:TCP/TCP connect & close.png)
下面我们来看下基本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:
-
passive socket:用于listen client的connect。在server端只有1个。
-
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种情况:
-
Network Layer的sending buffer有足够空间可以完全接受message,这个时候
send(message)返回len(message)表示已成功发送的数据长度(这里是全部); -
Network Layer的sending buffer半满,只能接受message的一部分数据,这个时候这个时候
send(message)返回len(message_sent)表示已成功发送的数据长度(这里是部分); -
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种情况:
-
Network Layer的receiving buffer有足够空间可以完全接收length长度的bytes,
recv(length)返回成功接收的长度(这里是length);若是到达的数据长度小于length,则接收更没有问题。 -
Network Layer的receiving buffer半满,可接收部分长度的bytes,
recv(length)返回成功接收的数据(这里是message_part); -
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也卡住。解决的办法有两种:
-
设置socekt options,当
send()和recv()block的时候马上返回; -
通过多线程或者运行系统的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个参数:
-
SHUT_WR:调用该方法的socket只能接受不能发送,peer只能发送,不能接收(得处理end-of-file情况)。
-
SHUT_RD:调用该方法的socket只能发送不能接收,peer只能接收,不能发送。
-
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/TCP总结.png)