上篇文章主要总结了 UDP 套接字的通信方式,这篇文章主要讲解 TCP 套接字的通信方式。
TCP 通信流程
一台主机可以即作为服务端又作为客户端,因此在 UDP 通信流程中将二者串联起来了,没有进行区分。由于 TCP 通信相对于 UDP 通信要复杂一点,这里将 TCP 通信拆分为服务端和客户端,方便理解。
首先是服务端的通信流程:
- 创建服务端套接字
- 绑定本机 IP 地址和端口(不必须)
- 将主动套接字转换为被动套接字(不必须)
- 监听客户端请求
- 接收客户端请求
- 发送/接受消息
- 关闭客户端套接字
- 关闭服务端套接字
客户端的通信流程如下:
- 创建客户端套接字
- 绑定本机 IP 地址和端口(不必须)
- 连接服务端
- 发送/接受消息
- 关闭客户端
下面依次进行讲解。
服务端通信流程
首先看一下服务端的通信流程。
创建服务端套接字
使用 TCP 通信,需要创建 TCP 的套接字:
sSocket = socket(AF_INET, SOCK_STREAM)
绑定服务器的 IP 和端口
sSocket.bind((IP, PORT))
同使用 UDP 套接字,绑定 IP 和端口是不必须的,如果要绑定本机上所有合法的 IP,可以这样写:
sSocket.bind(("",PORT))
将主动套接字转换为被动套接字
主动套接字是创建 TCP 套接字对象后的默认行为,只能向其他的主机发送消息,如果需要接收其他主机的消息,就需要将其转换为被动套接字,转换后就可以同时进行收发了,如果我们的主机不想接收其他主机的消息,就不需要转换为被动套接字,因此这项也不是必须的。
将主动套接字转换为被动套接字很简单,只需要调用套接字对象的 listen
方法:
sScoket.listen( maxConnect )
listen
函数接受一个参数,表示最大连接数。
接收客户端请求
执行 accept
方法以接收客户端的请求,该方法是一个阻塞方法:
clientSocket, clientInfo = sSocket.accept()
accept
方法返回一个元组,元组的第一项一个新的套接字对象,专门用来处理和相应的客户端的通信,元组的第二项是客户端的 IP 和端口信息。
发送/接受消息
发送消息使用 send
方法,接受消息使用 recv
方法:
# 发送消息
clientSocket.send(byte)
# 接收消息
recvData = clientSocket.recv( maxLen )
send
方法用来发送信息,接受一个 byte
类型的消息。
recv
方法用来接收信息,接受一个最大接收长度作为参数。
注意:以上两个方法都由特定的客户端套接字对象调用。
关闭套接字
通信完成后需要关闭套接字,首先需要关闭客户端套接字,最后当所有的客户端消息处理完成后,需要关闭服务端套接字:
# 关闭客户端套接字
clientSocket.close()
# 关闭服务端套接字
sScoket.close()
客户端通信流程
下面讲解客户端的通信流程,由于前面已有介绍,这里就不再赘述绑定和关闭套接字了。
创建套接字
首先需要在客户端创建一个套接字对象:
cSocket = socket( AF_INET, SOCK_STREAM)
连接服务端
连接服务端需要使用 connect
方法,该方法接受服务端的 IP 地址和对应的端口作为参数:
cSocket.connect( IP, PORT )
该方法也是阻塞的,连接过程会耗费一定时间。连接成功后,会触发客户端 Socket 对象的 accept
方法。
发送/接收消息
使用 send
方法向服务端发送消息,使用 recv
从服务端接收消息:
# 发送消息
cSocket.connect( msg )
# 接收消息
cSocket.recv( maxLen )
由于前面已经使用 connect
将客户端和服务端进行了连接,因此在使用 send
方法的时候不必再传入服务端相关的 IP 和端口信息了。
简单实例
下面做一个客户端和服务端通信的例子。首先是服务端代码 server.py:
from socket import *
def main():
# 创建服务端 socket 对象
sSocket = socket(AF_INET, SOCK_STREAM)
# 绑定本机端口
sSocket.bind(("",3001))
# 转换为被动套接字
sSocket.listen(5)
# 下面是一个轮询,在每次有客户端请求时进行处理
while True:
# 监听客户端请求
clientSocket,clientAddr = sSocket.accept()
print("%s 已连入,正在接受消息..."%clientAddr[0])
while True:
# 接受客户端消息
try:
recvMsg = clientSocket.recv(1024)
except:
# 如果触发异常,说明客户端断开了连接
print("%s 已断开连接~"%clientAddr[0])
break
print("%s:%s"%(clientAddr[0],recvMsg.decode("utf-8")))
# 回复消息
clientSocket.send("ding~".encode("utf-8"))
# 关闭客户端套接字
clientSocket.close()
if __name__ == '__main__':
main()
接着是客户端的代码,client.py:
from socket import *
def main():
cSocket = socket(AF_INET, SOCK_STREAM)
cSocket.connect(("192.168.2.142",3001))
while True:
msg = input("Enter Message:")
if msg == "q!":
break
else:
cSocket.send(msg.encode("utf-8"))
cSocket.close()
if __name__ == '__main__':
main()
我们看到 server.py 中有两个死循环,最外层的死循环用于和其他主机进行连接,连接成功后调用内存循环,用来和客户机通信。通信结束后,跳出内层循环,等待下一次连接。
如果客户端关闭了连接,服务端的 recv
方法在运行时会产生异常,我们可以通过异常的捕获来判断客户端是否断开了连接。
下面是运行效果:
实例改进
上面的实例有个问题:服务器一次只能处理一个 Socket 通信,只有在上一个 Socket 通信处理完成后,才能进行下一次通信的处理。这是因为外层的 while
循环需要内层的 while
循环执行完成后再执行,其他时间它都是阻塞的,只有这一次的 Socket 的通信处理完了,外层的 while
循环才能进行下一次的处理。两个客户端同时请求服务端的运行效果如下:
要解决这个问题也很简单,既然是多连接多任务处理,我们只需将内层的
while
循环放在进程中,此后每新增一个连接,就为其开一个进程进行处理,实现并发操作。修改 server.py:
from socket import *
from multiprocessing import Process
class Server():
@classmethod
def __prepareSocket(cls):
# 将服务端的 Socket 对象作为类成员
cls.sSocket = socket(AF_INET, SOCK_STREAM)
cls.sSocket.bind(("",3001))
cls.sSocket.listen(5)
@classmethod
def startServer(cls):
cls.__prepareSocket()
while True:
# 监听客户端请求
clientSocket,clientAddr = cls.sSocket.accept()
print("%s 已连入,正在接受消息..."%clientAddr[1])
cp = SocketHander(clientSocket,clientAddr)
cp.start()
class SocketHander(Process):
def __init__(self,clientSocket,clientAddr):
Process.__init__(self)
self.clientSocket = clientSocket
self.clientAddr = clientAddr
def run(self):
# 将内层循环至于 run 方法中
while True:
try:
recvMsg = self.clientSocket.recv(1024)
except:
# 如果触发异常,说明客户端断开了连接
print("%s 已断开连接~"%self.clientAddr[0])
break
print("%s:%s"%(self.clientAddr[0],recvMsg.decode("utf-8")))
# 回复消息
self.clientSocket.send("ding~".encode("utf-8"))
# 关闭客户端套接字
self.clientSocket.close()
if __name__ == '__main__':
Server.startServer()
看下效果:
完。