tcp连接

连接状态

tcp本身就是长连接+全双工的,什么意思呢?就是tcp连接一旦建立,就会一直维持连接状态,这个状态下,服务端和客户端是可以同时发送和接收数据的。注意是同时,也就是说发送的同时也能接收,它们是两块独立的缓冲区。下面是python代码,服务端和客户端建立连接后,就启用两个线程,一个用于读数据,一个用于发数据。

# server.py
import socket,time
import threading
host = '127.0.0.1'
port = 12345

def read(c):
while True:
data = c.recv(1024)
print(f"server recv: {data.decode('utf-8')}")

def write(c):
while True:
v = input()
c.sendall(v.encode('utf-8'))

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.bind((host, port))
s.listen()
print("server start...")

conn, addr = s.accept()
print(f"client connect, {addr}")
threading.Thread(target=read, args=(conn,)).start()
threading.Thread(target=write, args=(conn,)).start()

time.sleep(9999)

conn.close()
s.close()
# client.py
import socket
import threading
import time
host = '127.0.0.1'
port = 12345

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.connect((host, port))

threading.Thread(target=read, args=(s,)).start() # 同server的read()方法
threading.Thread(target=write, args=(s,)).start()

time.sleep(9999)

s.close()

几个点需要注意:

1. 在连接没有断开之前(代码中的sleep(9999)作用就是阻止调用close()),两端可以任意发送和接收数据,发送多少次都可以,这也是目前长连接或者websocket的实现基础。

2. 默认情况下,recv()函数在连接建立的状态下是阻塞的,也就是说,如果对方没有向写recv缓冲区放数据的话,该方法会一直阻塞

3. 连接断开的情况下,recv()函数就会立刻返回空字符串。这里又有三点需要注意:

  1. 连接断开,指的是对方不管正常还是异常退出。
  2. 立即返回空字符串,即“”,而不是None
  3. 连接断开后,数据接收方的recv()是会一直接收一个空串的,此时已经不阻塞了,如果recv()在循环体内,则会一直循环

下图展示了客户端正常退出后,两者的tcp状态。tcp规定发起退出的一方会在最后进入CLOSE_WAIT状态,以等待该连接彻底失效。而此时,由于我上面的代码并没有判断正常退出的流程,所以理论上它们两者的状态会一直维持现状。

解释一下这两个状态产生的过程:客户端要关闭连接了,给服务端发送了一个关闭tcp连接的报文(FIN),此时客户端会进入FIN_WAIT1状态等待服务端确认,服务端收到报文后会自动回复一个ACK,此时服务端进入CLOSE_WAIT状态,而客户端收到该ACK后就进入FIN_WAIT2状态,这两个状态是相辅相成的,其本质是等待服务端将剩下的数据传输完成,注意这些步骤都是tcp完成的,而不是我们代码实现。实际上tcp并不可能知道到底什么时候服务端会将数据传输完成,所以它会一直等待业务逻辑代码执行完,直到调用close()方法,而我上述代码中,它会sleep很久,所以这个状态会一直保持。理论上tcp协议会将这个状态保持到系统关机,实际上系统也会设定一个超时时间。

所以正常情况下,当调用recv()返回空字符串时,你就应该意识到对方已经关闭了,所以你这边也应该调用close()方法了。

那么如果我就是给对方发送了一个空字符串呢?这是不被允许的,空字符串是发不出去的,必须有内容。

当然,也可以设置recv()不阻塞,s.setblocking(False),这种情况下,无论如何recv()都会立即返回,但是如果没有数据的话,就会报异常“BlockingIOError: [Errno 11] Resource temporarily unavailable”。此时可以使用try对其封装,或者使用select模块,这就是非阻塞io或NIO的实现方式了。

telnet

telnet一般用于shell登录,但很多时候也会用它来看端口监听情况。例如上文中的server开启后,我就可以用下面的命令查看端口是否正常监听:

telnet 127.0.0.1 12345

如果是监听状态,则会显示已连接,此时,实际上就是建立了一个原始的tcp连接,你在telnet中输入的内容都会不加修饰得发送给服务端

注:为什么每个接收的数据后面都有空行呢,是因为用telnet发送数据的时候按的回车键,就在内容后面加了换行符了

也就是说,如果此时你按照http的协议来写协议头内容,你甚至可以会得到html

按照telnet提示,在这个界面按下 crtl+],则会进入telnet的命令行,此时就可以输入telnet的命令,例如关闭连接、退出登录、等等

Leave a Comment