在学习网络编程时,需要预先了解大量的概念,因此,对于没有任何基础的朋友,请先阅读本公众号内的【认识计算机】系列文章中的《5. 计算机与网络》教程后,再学习本章。
通常网络编程也被称为套接字
(socket)编程,它最早可以追溯到 20 世纪 70 年代,在美国加利福尼亚大学的伯克利版本 UNIX系统(即BSD UNIX)上出现。
C指Client
即客户端,S指Server
即服务端。C/S架构也就是所谓的客户端/服务端架构。简单来说,客户端也就是我们用户电脑上的程序,而服务端则是远程的用于接收、处理来自客户端发送的数据的程序。
因此我们知道,通常客户端和服务端并不是运行在一起的,但是由于实际情况的限制,我们今天要学习编写的程序,是使客户端和服务端运行在同一台电脑上的。
在之前的理论知识部分,我们已经知道,涉及到网络编程方面,基本就是和协议打交道了。现在我们要学习的第一个协议就是TCP协议。
TCP协议是一种面向连接的套接字。这意味着在进行通信之前必须先建立一个连接。TCP连接是基于字节流的,通过TCP连接传送的数据,无差错、不丢失、不重复,且按顺序到达。
TCP协议这种所谓的必须先建立连接才能通信的模型,非常像我们打电话的过程。我们打电话时,必须等到电话彻底接通后,才能开始讲话,否则在电话仍处于“嘟…嘟…”声时,我们讲话是没用的。与之相反的,则是一种无连接通信。大家可以回忆一下,曾经有一种通信工具叫BP机,或者俗称Call机
,在中国香港老电影中还有见到。给对方BP机发送完信息后就结束了,并不需要对方的BP机处于开机状态,即使对方关机了,开机后也可以可以收到信息。
实际上真正最接近无连接通信的是电报机。如果大家喜欢看谍战影片,对于发电报应该不会陌生。发电报的人将电报发出去就结束了,如果此刻对方没有监听电报的无线信号,那么就错过了该电报,这意味着信息丢失,通信失败。因此电报通信的双方在通信前必须约定好时间,几点几分开始通信,然后一份电报要连续重复发送三遍,防止对方开小差,没监听到电报信号。
而面向连接的通信则不同,这就好比通信双方建立了一个类似水管的通道,数据就是水管中的水,只有通道建立成功之后,才会开闸放水,确保水流通畅。一旦水管断裂,立即宣告通信失败,并尝试重新建立通道,直到通道成功建立才再次通信。因此才说,面向连接的通信是可靠的,安全的,不会丢失数据的。
创建client.py
文件
1 import socket
2
3 # 定义IP地址和端口号
4 ip = '127.0.0.1'
5 port = 8787
6
7 # 这里通过socket函数创建一个客户端套接字对象client_sock
8 # AF_INET:用于指定地址族,这里是固定写法,它表示使用IPv4地址
9 # SOCK_STREAM:表示指定使用TCP协议
10 client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
11
12 # 连接指定的IP地址和端口,创建一个TCP连接通道
13 client_sock.connect((ip, port))
14
15 # 向对方发送信息。TCP协议发送的是字节
16 # 因此需要先调用encode函数将字符串转为字节,才能发送出去
17 client_sock.send("Hello ,I'm client".encode())
18
19 # 关闭连接
20 client_sock.close()
直接运行上述代码会报错,因为没有服务端程序,无法建立连接
创建server.py
文件
1 import socket
2
3 # 定义IP地址和端口号
4 ip = '127.0.0.1'
5 port = 8787
6
7 print("**** 服务端启动 ****")
8
9 # 创建服务端套接字对象
10 server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
11
12 # 绑定IP和端口号
13 server_sock.bind((ip, port))
14
15 # 开始监听IP和端口,设置最多接受5个客户端访问
16 server_sock.listen(5)
17
18 # 等待客户端来连接。accept函数会阻塞,即在此处暂停,直到有客户端连接才继续向下执行
19 # accept返回两个值,第一个是客户端对象,第二个是客户端的地址
20 client, addr = server_sock.accept()
21
22 print("来了一个新客户端:", addr[0])
23
24 # 从已连接的通道中读取1024字节大小的数据,即1K大小
25 data = client.recv(1024)
26
27 # TCP传送的是字节,因此调用decode函数将字节转为字符串
28 print(data.decode())
29
30 # 关闭连接
31 client.close()
32 server_sock.close()
在运行时需要注意,服务端和客户端是两个程序,这里建议使用命令行运行,而不要在IDE中运行代码。打开两个命令行窗口,首先用一个运行服务端程序,然后另一个运行客户端程序
可以看到,服务端成功收到了来自客户端发送的信息。
以上代码中,有几点需要特别说明一下
127.0.0.1
做为IP地址? 127.0.0.1
是一个特殊的IP地址,它是指本机网卡的回送IP地址,一般用于测试,大家一定要记住这个地址。其原理如下图 当然,如果大家有两台电脑,并且这两台电脑处于同一个局域网中,例如连接在同一个路由器上,那么就无需如此模拟,可以使用实际IP地址来验证程序。
经过上面的学习,我们对TCP网络编程已经有了一定的理解,下面就让我做一点稍微复杂的程序,写一个单步的聊天程序。
1 import socket
2 import time
3
4 # 定义IP地址和端口号
5 ip = '127.0.0.1'
6 port = 8787
7
8 print("**** 服务端启动 ****")
9
10 # 创建服务端套接字对象
11 server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
12
13 # 绑定IP和端口号
14 server_sock.bind((ip, port))
15
16 # 开始监听IP和端口,设置最多接受5个客户端访问
17 server_sock.listen(5)
18
19 # 等待客户端来连接。accept函数会阻塞
20 client, addr = server_sock.accept()
21 print("来了一个客户端:", addr[0])
22 client.send("!!!欢迎访问TCP服务器!!!".encode())
23
24 # 开启一个死循环,使服务器一直运行
25 while True:
26 # 从已连接的通道中读取1024字节大小的数据
27 data = client.recv(1024)
28
29 # 获取当前电脑的时间
30 this_time = time.strftime("%H:%M:%S")
31 print("from client:", this_time)
32 print(data.decode())
33
34 message = input("请输入:")
35 client.send(message.encode())
36
37 client.close()
38 server_sock.close()
1 import time
2 import socket
3
4 # 定义IP地址和端口号
5 ip = '127.0.0.1'
6 port = 8787
7
8 # 创建一个客户端套接字对象client_sock
9 client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
10
11 # 连接指定的IP地址和端口,创建一个TCP连接通道
12 client_sock.connect((ip, port))
13
14 while True:
15 data = client_sock.recv(1024)
16
17 # 获取当前电脑的时间
18 this_time = time.strftime("%H:%M:%S")
19 print("from server:", this_time)
20 print(data.decode())
21
22 message = input("请输入:")
23 client_sock.send(message.encode())
24
25 client_sock.close()
我们已经说过,TCP通信,是首先建立一个连接通道,然后使用这个通道通信。既然是通道,那么在这个通道的两端,当然是既可以往通道里发数据,也可以从通道里面收数据了,这个通道是可以同时收发的。
打开两个命令行工具,运行以上代码后,会发现一个问题,那就是这个聊天工具是单步的,整个聊天过程是交替进行的,说完一句话必须等对方说,对方说完自己才能接着回。这是因为我们还没有学过并发,我们现在写的程序都是单步的,不能同时做多件事,以后学会了并发编程,再改造这个程序,就能实现收发自如了。