先行知识

TCP 是面向连接的,因此 TCP socket 编程在客户端发送消息前需要创建连接。同样的,TCP 的客户端与服务器之间收发的数据只有消息,没有 ip port 元组,因为之前已经建立了连接。

TCP 要区分客户端与服务端,二者的编写差异较大。

with as 的适用场景:

如果你打开时没有产生异常,反而在读写文件过程中产生了异常,但是你最终还是要关闭文件的,所以你需要用 try 来捕获读写过程中的异常,而 Python 使用 with 便可以解决这样的问题。

1
2
3
4
5
f = open('xxx')
try:
f.write()/read()
except:
f.close()

上面这一段代码便可以使用 with as 写成下面的样子:

1
2
with open('xxx') as f:
f.write()/read()

注意:with 中不需要写 close ,它会保证关闭文件。

with open(文件名字符串,打开方式) as f

​ f.write(内容字符串)

with as 不论是否写入内容是否存在都会关闭文件。

注意:如果没有要打开的文件或打开文件报错,用 with as 也没啥用了,此时,就该用 try catch 了。一般使用 with as 以写的方式打开文件,因为那样,没有就会创建一个文件,在文件打开处,就不会报错。所以如果你要读文件,还是用 try catch。

TCP 通信模型

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import socket


def main():
# 1. 创建 tcp 套接字
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2. 创建与服务器的 TCP 链接
# tcp_socket.connect(('192.168.33.11',7989))
server_ip = input('请输入要链接的服务器 ip:')
server_port = int(input('请输入要链接的服务器的 port:'))
server_addr = (server_ip,server_port)
tcp_socket.connect(server_addr)

# 3. 发送的数据/接收数据
send_data = input('请输入要发送的数据:')
tcp_socket.send(send_data.encode('gbk')) # tcp 发送消息使用send,而且参数只有消息,因为前面已经建立过链接了。

# 4. 关闭套接字
tcp_socket.close()

if __name__ == '__main__':
main()

注意: TCP 使用 SOCK_STREAM, 与 UDP 不同, TCP 是基于连接的,所以它只需要一开始创建一个 TCP 连接,之后发送消息便不用再向 UDP 那样指定目标程序的 IP 以及 端口(这也就是 TCP 的 send 函数与 UDP 的 sendto 函数的不同)。

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
import socket


def main():
# 1. 创建套接字
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2. 绑定本地信息 bind
tcp_server_socket.bind(('',45586))

# 3. 让默认的套接字由主动变被动
tcp_server_socket.listen(128) # 为什么是 128

while True:
print('等待一个新的客户端到来...')
# 4. 等待客户端链接 accept
client_socket, client_addr = tcp_server_socket.accept()

print("一个新的客户端到来:%s" % str(client_addr))

while True:
# 接收客户端发来的请求
recv_data = client_socket.recv(1024)
if recv_data:
print("客户端发过来的请求时:%s" % recv_data.decode('gbk'))

# 发送信息
client_socket.send(('GoodBye MyLove!').encode('gbk'))
else:
break

# 关闭客户端套接字 关闭 accept 返回的套接字意味着不会再为这个客户端进行服务。
client_socket.close()
print('已经服务完毕!')

# tcp 关闭套接字, 关闭监听套接字,会导致不再等待新的客户端到来,即:xxx.aceept() 失败
tcp_server_socket.close()

if __name__ == '__main__':
main()

TCP 的服务端必须绑定端口。TCP 客户端没有必要绑定,一方面是避免固定端口与别的冲突,另一方面便于多开。

listen 使套接字变为可以被动监听,accept 会进入阻塞状态,等待客户端的链接。

listen() 中的 128 这个值与连接服务器的客户端的个数有一点儿关系,但是并不是绝对的,写 128 256 7 10 没太大区别,具体与操作系统有关。

accept() 返回一个元组,我们直接拆包使用就行了。

recv() 返回值只有数据, recvfrom() 返回值还有 ip port

如果要一致处理请求的话,在 accept 之前 使用 while True,因为 accpet 要进入阻塞状态,等待客户端连接。

TCP 的服务端可以同时有多个客户端连接,但是先来的客户端先得到服务,后面的虽然连接上了,却得不到服务。tcp 服务端一次只能服务一个人。

recv 会堵塞,等待客户端发送过来数据。

recv 解堵塞有两种情况:

  1. 接收到了客户端的消息
  2. 客户端关闭了套接字。

客户端关闭套接字,recv 的返回值为空。(注:如果是客户端发送消息,发送不了空的消息。)

我们通过判断 recv 的返回值来判断客户端是否 close

if 后面可以放数字,字符串,元组,集合,字典,列表和 None,True,False

数字:不是 0 成立;

字符串,元组,集合,字典,列表:非空成立

不是 None,不是 False 成立

True 成立

总结:

UDP TCP Client TCP Server
socket socket socket
bind(如果要接收消息的话) connect bind
sendto/recvfrom send listen
close close accept
recv/send
close

QQ 的整体架构(了解)

qq1 登录 tcp 连接腾讯服务器(有固定端口)

qq2 登录 tcp 连接腾讯服务器(有固定端口)

qq1 向 qq2 发消息:消息从 qq1 到达腾讯服务器,腾讯服务根据你的好友 qq2 的信息,找到其 ip 和端口的套接字,然后发送到 qq2。

所以腾讯服务器充当一个转发和监视功能(emmm, 你懂得,小心说话)。