TCPIP网络编程之优雅的断开套接字

TCP/IP网络编程之优雅的断开套接字

linux的close函数意味着完全断开连接,完全断开连接不仅指无法传输数据,而且也不能接收数据。因此,在某些情况下,通信一方调用close函数断开连接就显得不太优雅,如图:

​ 单方面断开连接

上图描述的是两台主机正在进行双向通信,主机A发送完最后的数据后,调用close函数断开连接,之后主机A无法再接收主机B传输的数据。实际上,是完全无法调用与接收数据相关的函数。最终,由主机B传输而主机A接收的数据也销毁了。

首先需要区分一下关闭socket和关闭TCP连接的区别,关闭TCP连接是指TCP协议层的东西,就是两个TCP端之间交换了一些协议包(FIN,RST等),具体的交换过程可以看TCP协议,这里不详细描述了。而关闭socket是指关闭用户应用程序中的socket句柄,释放相关资源。但是当用户关闭socket句柄时会隐含的触发TCP连接的关闭过程。

TCP连接的关闭过程有两种,一种是优雅关闭(graceful close),一种是强制关闭(hard close或abortive close)。所谓优雅关闭是指,如果发送缓存中还有数据未发出则其发出去,并且收到所有数据的ACK之后,发送FIN包,开始关闭过程。而强制关闭是指如果缓存中还有数据,则这些数据都将被丢弃,然后发送RST包,直接重置TCP连接。

close方法可以释放一个连接的资源,但是不是立即释放,如果想立即释放,那么在close之前使用shutdown方法

  • SHUT_RD:断开输入流。套接字无法接收数据(即使输入缓冲区收到数据也被抹去),无法调用输入相关函数。
  • SHUT_WR:断开输出流。套接字无法发送数据,但如果输出缓冲区中还有未传输的数据,则将传递到目标主机。
  • SHUT_RDWR:同时断开 I/O 流。相当于分两次调用 shutdown(),其中一次以 SHUT_RD 为参数,另一次以 SHUT_WR 为参数。

使用:在close()之前加上shutdown(num)即可 [shut_rd(), shut_wr(), shut_rdwr()分别代表num 为0 1 2 ]

也就是说,想要关闭一个连接,首先把通道全部关闭,然后在release连接,以上三个静态变量分别对应数字常量:0,1,2

close()和shutdown()的区别

确切地说,close() 用来关闭套接字,将套接字描述符(或句柄)从内存清除,之后再也不能使用该套接字,与C语言中的 fclose() 类似。应用程序关闭套接字后,与该套接字相关的连接和缓存也失去了意义,TCP协议会自动触发关闭连接的操作。

shutdown() 用来关闭连接,而不是套接字,不管调用多少次 shutdown(),套接字依然存在,直到调用 close() 将套接字从内存清除。

调用 close() 关闭套接字时,或调用 shutdown() 关闭输出流时,都会向对方发送 FIN 包。FIN 包表示数据传输完毕,计算机收到 FIN 包就知道不会再有数据传送过来了。

默认情况下,close() 会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据,而shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包。也就意味着,调用 close() 将丢失输出缓冲区中的数据,而调用 shutdown() 不会。

shutdown用于以下几种情况:

通常来说,socket是双向的,即数据是双向通信的。但有些时候,你会想在socket上实现单向的socket,即数据往一个方向传输。

单向的socket便称为半开放Socket。要实现半开放式,需要用到shutdown()函数。

一般来说,半开放socket适用于以下场合:

(1)当你想要确保所有写好的数据已经发送成功时。如果在发送数据的过程中,网络意外断开或者出现异常,系统不一定会返回异常,这是你可能以为对端已经接收到数据了。这时需要用shutdown()来确定数据是否发送成功,因为调用shutdown()时只有在缓存中的数据全部发送成功后才会返回。

(2)想用一种方法来捕获程序潜在的错误,这错误可能是因为往一个不能写的socket上写数据,也有可能是在一个不该读操作的socket上读数据。当程序尝试这样做时,将会捕获到一个异常,捕获异常对于程序排错来说是相对简单和省劲的。

(3)当您的程序使用了fork()或者使用多线程时,你想防止其他线程或进程访问到该资源,又或者你想立刻关闭这个socket,那么可以用shutdown()来实现。

另外说一下,如果调用了Close()函数,程序中只是确保了对于某个特定的进程或线程来说,该连接是关闭的;但socket只有在所有的进程调用了Close()或者socket超出了工作范围时,才会真正的被关闭或删除。而如果想立刻关闭socket,那么可以通过shutdown()来实现。

shutdown()的调用是需要一个参数:0代表禁止下次的数据读取;1代表禁止下次的数据写入;2代表禁止下次的数据读取和写入。

同时,shutdown()的效果是累计的,不可逆转的。既如果关闭了一个方向数据传输,那么这个方向将会被关闭直至完全被关闭或删除,而不能重新被打开。如果第一次调用了shutdown(0),第二次调用了shutdown(1),那么这时的效果就相当于shutdown(2),也就是双向关闭socket。

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
65
66
服务端

from socket import *
import threading,os,time

class Server():
def __init__(self,host='127.0.0.1',port=9990):
try:
addr=(host,port)
self.tcpSerSock=socket(AF_INET,SOCK_STREAM)
self.tcpSerSock.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
self.tcpSerSock.bind(addr)
self.tcpSerSock.listen(5)
except Exception,e :
print 'ip or port error :',str(e)
self.tcpSerSock.close()

def main(self):
while 1 :
try :
print 'wait for connecting ...'
tcpCliSock,addr = self.tcpSerSock.accept()
addrStr = addr[0]+':'+str(addr[1])
print 'connect from',addrStr
except KeyboardInterrupt:
self.close=True
tcpCliSock.close()
self.tcpSerSock.close()
print 'KeyboardInterrupt'
break
ct = ClientThread(tcpCliSock,addrStr)
ct.start()

class ClientThread(threading.Thread):
def __init__(self,tcpClient,addr):
super(ClientThread,self).__init__()

self.tcpClient = tcpClient
self.addr = addr
self.timeout = 60
tcpClient.settimeout(self.timeout)
self.cf = tcpClient.makefile('rw',0)

def run(self):
while 1:
try:
data = self.cf.readline().strip()
if data:
if data.find("set time")>=0:
self.timeout = int(data.replace("set time ",""))
self.tcpClient.settimeout(self.timeout)
print self.addr,"client say:",data
self.cf.write(str(self.addr)+" recevied ok!"+"\n")
else:
break
except Exception,e:
self.tcpClient.close()
self.cf.write("time out !"+"\n")
print self.addr,"send message error,",str(e)
#此处将break注释掉
# break


if __name__ == "__main__" :
ser = Server()
ser.main()
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
客户端
from socket import *
class Client():
def __init__(self):
pass

def main(self):
tcpCliSock=socket(AF_INET,SOCK_STREAM)
tcpCliSock.connect(('127.0.0.1',9990))
print 'connect server 9999 successfully !'
cf = tcpCliSock.makefile('rw', 0)
while 1:
data=raw_input('>')
try:
if data:
cf.write(data+"\n")
data = cf.readline().strip()
if data:
print "server say:",data
else:
break
else:
break
except Exception,e:
print "send error,",str(e)
if __name__ == "__main__":
cl = Client()
cl.main()

在代码中可以看出,如果timeout后,except肯定能够捕获到timeout异常,这样就会进入到except代码中,在上面我们故意将break注释掉,也就是不让其跳出循环,经过试验,可以得知,虽然在server端已经将连接close掉了,但是client端仍然可以顺利的接收到消息,而且,如果client端发送数据的间隔小于超时时间的话,此连接可以顺利的一直使用,这样,我们close貌似就一点儿效果都没有了
要实现我们的功能,只需要改变server端的代码

1
2
3
4
5
6
except Exception,e:
self.tcpClient.shutdown(2)
self.tcpClient.close()
self.cf.write("time out !"+"\n")
print self.addr,"send message error,",str(e)
break