网络通信概述
网络:⽹络就是⼀种辅助双⽅或者多⽅能够连接在⼀起的⼯具。
使⽤⽹络的⽬的:
1.联通多⽅然后进⾏通信⽤的,即把数据从⼀⽅传递给另外⼀⽅
2.⽤⽹络能够把多⽅链接在⼀起,然后可以进⾏数据传递
3.⽹络编程就是,让在不同的电脑上的软件能够进⾏数据传递,即进程之间的通信
TCP/IP协议
首先什么是协议呢?
有的说英语,有的说中⽂,有的说德语,说同⼀种语⾔的⼈可以交流,不同的语⾔之间就交流不了了,为了解决不同种族⼈之间的语⾔沟通障碍,现规定国际通⽤语⾔是英语,这就是⼀个规定,这就是协议。
不同种类之间的计算机到底是怎么进⾏数据传递的呢?
就像说不同语⾔的⼈沟通⼀样,只要有⼀种⼤家都认可都遵守的协议即可, 那么这个计算机都遵守的⽹络通信协议叫做 TCP/IP协议。
下面看几个网络里的专业术语:
TCP/IP协议(族)
互联⽹协议包含了上百种协议标准,但是最重要的两个协议是TCP和IP协议,所以,⼤家把互联⽹的协议简称TCP/IP协议 。如下图:
IP地址: ⽤来在⽹络中标记⼀台电脑的⼀串数字,⽐如192.168.1.1;在本地 局域⽹上是惟⼀的。
IP地址的分类:每⼀个IP地址包括两部分:⽹络地址和主机地址,如下图:
私有IP
⽹络IP中,国际规定有⼀部分IP地址是⽤于局域⽹使⽤,也就是属于私⽹IP,不在公⽹中使⽤的,它们的范围是:
- 10.0.0.0~10.255.255.255
- 172.16.0.0~172.31.255.255
- 192.168.0.0~192.168.255.255
回环地址IP
IP地址127.0.0.1 代表本机IP地址,等价于localhost, ⽤ http://127.0.0.1 就可以测试本机中配置的Web服务器。
⼦⽹掩码
⼦⽹掩码不能单独存在,它必须结合IP地址⼀起使⽤。对于A类地址来说,默认的子网掩码是255.0.0.0;对于B类地址来说默认的子网掩码是255.255.0.0;对于C类地址来说默认的子网掩码是255.255.255.0。
⼦⽹掩码的作⽤: 它是一种用来指明一个IP地址的哪些位标识的是主机所在的子网,以及哪些位标识的是主机的位掩码。子网掩码只有一个作用,就是将某个IP地址划分成网络地址和主机地址两部分。⼦⽹掩码的设定必须遵循⼀定的规则,用来判断两个IP是否在同一个网络。
规则:子网掩码的设定必须遵循一定的规则。与二进制IP地址相同,子网掩码由1和0组成,且1和0分别连续。子网掩码的长度也是32位,左边是网络位,用二进制数字“1”表示,1的数目等于网络位的长度;右边是主机位,用二进制数字“0”表示,0的数目等于主机位的长度。这样做的目的是为了让掩码与ip地址做按位与运算时用0遮住原主机数,而不改变原网络段数字,而且很容易通过0的位数确定子网的主机数。
端口
如果把IP地址比作一间房子 ,端口就是出入这间房子的门。真正的房子只有几个门,但是一个IP地址的端口可以有65536(即:2^16)个之多!端口是通过端口号来标记的,端口号只有整数,范围是从0 到65535(2^16-1)。 每种网络的服务功能都不相同,因此有必要将不同的封包送给不同的服务来处理,当你的主机同时开启了FTP与WWW服务时,别人送来的资料封包,就会依照 TCP 上面的 port 号码来给 FTP 这个服务或者是 WWW 这个服务来处理。
socket编程
我们知道在本地进程间通信的方式有队列、同步(互斥锁、条件变量等)和管道,并且本地通过进程PID来唯⼀标识⼀个进程,那么在⽹络中是如何唯⼀标识⼀个进程的呢?
⽹络层的“IP地址”可以唯⼀标识⽹络中的主机,⽽传输层的“协议+端⼝”可以唯⼀标识主机中的应⽤程序(进程)。因此利用IP地址,协议,端⼝就可以标识⽹络的进程了。
什么是socket?
socket(简称套接字) 是进程间通信的⼀种⽅式, 能实现不同主机间的进程间通信,我们⽹络上各种各样的服务⼤多都是基于 Socket 来完成通信的。
创建socket
在 Python 中使⽤socket 模块的函数 socket 就可以完成创建:
socket.socket(AddressFamily, Type)
1). Address Family: AF_INET: IPV4⽤于 Internet 进程间通信 AF_INET6: IPV6⽤于 Internet 进程间通信
2). Type:套接字类型 SOCK_STREAM: 流式套接字,主要⽤于TCP协议 SOCK_DGRAM: 数据报套接字,主要⽤于 UDP协议
下面我们用代码根据要求创建两个socket ,创建⼀个tcp socket(tcp套接字),创建⼀个udp socket(udp套接字):
import socket
socketObj1 = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
socketObj2 = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
UDP
介绍:UDP ⽤户数据报协议,是⼀个⽆连接的简单的⾯向数据报的运输层协议。UDP不提供可靠性,它只是把应⽤程序传给IP层的数据报发送出去,但是并不能保证它们能到达⽬的地。由于UDP在传输数据报前不⽤在客户和服务器之间建⽴⼀个连接,且没有超时重发等机制,故⽽传输速度很快。 UDP是⼀种⾯向⽆连接的协议,每个数据报都是⼀个独⽴的信息,包括完整的源地址或⽬的地址,它在⽹络上以任何可能的路径传往⽬的地,因此能否到达⽬的地,到达⽬的地的时间以及内容的正确性都是不能被保证的。
特点:
UDP是⾯向⽆连接的通讯协议,UDP数据包括⽬的端⼝号和源端⼝号信息, 由于通讯不需要连接,所以可以实现⼴播发送。 UDP传输数据时有⼤⼩限制,每个被传输的数据报必须限定在64KB之内。 UDP是⼀个不可靠的协议,发送⽅所发送的数据报并不⼀定以相同的次序到达接收⽅。如下图所示:
应用场景:
UDP是⾯向消息的协议,通信时不需要建⽴连接,数据的传输⾃然是不可靠的,UDP⼀般⽤于多点通信和实时的数据业务,⽐如:语⾳⼴播、视频、QQ、TFTP(简单⽂件传送)、SNMP(简单⽹络管理协议)和DNS(域名解释)。
UDP网络程序模型
下面我们基于这个网络程序模型,用Python代码实现UDP网络的服务端和客户端:
首先是UDP网络的服务端代码如下:
import socket
udpserver = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
# 绑定IP和端口
udpserver.bind(('0.0.0.0', 9999)) #0.0.0.0.0代表开放所有的IP地址
print('QQ用户A上线.........')
while True:
# 返回的是元组, 一个个元素是客户端发送的信息, 第二个元素是客户端和服务端交互的地址(IP, port)
recv_data, address = udpserver.recvfrom(1024)
# print(address)
print("B:>> ", recv_data.decode('utf-8'))
if recv_data == b'quit':
print("聊天结束.......")
break
# 发送的消息必须是bytes类型
# bytes --> str bytesObj.decode('utf-8')
# str --> bytes strObj.encode('utf-8')
send_data = input('A: >> ').encode('utf-8')
if not send_data:
continue
udpserver.sendto(send_data, address)
udpserver.close()
然后是UDP网络的客户端代码:
import socket
udpClient = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
print("用户B上线……")
while True:
send_data = input('B:>> ').encode('utf-8')
if not send_data:
continue
udpClient.sendto(send_data, ('172.25.254.4', 9999))
if send_data == b'quit':
print("聊天结束……")
break
recvData, address = udpClient.recvfrom(1024)
print("A:>> ", recvData.decode('utf-8'))
udpClient.close()
先运行服务端,可以看到服务端开启并等待连接,然后运行客户端,客户端就会和服务端成功进行通信。
UDP网络程序原理如下:
TCP
传输控制协议(英语:Transmission Control Protocol,缩写为TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
下面是一个TCP和UDP的对照表:
TCP网络程序的模型
TCP网络和UDP网络是有一点不同的 ,如下图所示:
下面我们基于这个网络程序模型,用Python代码实现TCP网络的服务端和客户端:
首先是TCP网络的服务端代码如下:
import socket
# 1. 创建服务端socket对象
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
# 2. 绑定地址和端口(IP:port)
server.bind(('0.0.0.0', 9998))
# 3. 监听是否有客户端连接?listen
server.listen(5)
print('server start .........')
# 4.接收客户端的连接accept
clientSocketObj, clientAddress = server.accept()
# 5. 接收客户端发送的消息
recv_data = clientSocketObj.recv(1024).decode('utf-8')
print("接收到客户端发送的消息:", recv_data)
# 6. 给客户端发送消息
send_data = b'hello client'
clientSocketObj.send(send_data)
# 7. 关闭socket对象
clientSocketObj.close()
server.close()
然后是TCP网络的客户端代码:
import socket
# 1. 创建服务端socket对象
client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
# 2. 连接服务端
client.connect(('172.25.254.197', 9998))
# 3.给服务端发送消息
client.send(b'hello server')
# 4. 接收服务端发送的消息
recv_data = client.recv(1024).decode('utf-8')
print("接收服务端发送的消息:", recv_data)
# 5. 关闭socket对象
client.close()
首先运行服务端,可以看到服务端开启并等待连接,然后运行客户端,客户端就会和服务端成功进行通信。
下面我们通过TCP网络程序模型实现一个模拟QQ聊天的案例,基本思路就是基于TCP网络程序服务端和客户端,然后在它们里面加上循环,可以让它们之间一直进行交流通信,直到传输指定信息比如‘quit’时退出。
下面是模拟QQ聊天的服务端代码:
import socket
# 1. 创建服务端socket对象
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
# 2. 绑定地址和端口(IP:port)
server.bind(('0.0.0.0', 9995))
# 3. 监听是否有客户端连接?listen
server.listen(5)
print('server start .........')
# 4.接收客户端的连接accept
clientSocketObj, clientAddress = server.accept()
while True:
# 5. 接收客户端发送的消息
recv_data = clientSocketObj.recv(1024).decode('utf-8')
print("接收到客户端发送的消息:", recv_data)
if recv_data == 'quit':
break
# 6. 给客户端发送消息
send_data = input('server:>> ').encode('utf-8')
if not send_data:
continue
clientSocketObj.send(send_data)
# 7. 关闭socket对象
clientSocketObj.close()
server.close()
然后下面是模拟QQ聊天案例的客户端代码:
import socket
# 1. 创建服务端socket对象
client = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
# 2. 连接服务端
client.connect(('172.25.254.197', 9995))
while True:
# 3.给服务端发送消息
send_data = input('client: >> ').encode('utf-8')
if not send_data:
continue
client.send(send_data)
# 4. 接收服务端发送的消息
recv_data = client.recv(1024).decode('utf-8')
print("接收服务端发送的消息:", recv_data)
if recv_data == 'quit':
break
# 5. 关闭socket对象
client.close()
先运行服务端,然后再运行客户端,就可以实现模拟QQ聊天,具体如下:
服务端界面:
客户端界面:
TCP详解
TCP三次握手:
TCP连接是通过三次握手来连接的。 图示如下:
两个包: 同步序列标号 SYN 确认包 ACK
四种状态: SYN_SENT, LISTEN, SYN_RECV,ESTABLISHED
在三次握手中,客户端和服务器端都发送两个包SYN和ACK,只不过服务器端的两个包是一次性发过来的,客户端的两个包是分两次发送的。具体解释如下:
第一次握手:当客户端向服务器发起连接请求时,客户端会发送同步序列标号SYN到服务器,在这里我们设SYN为x,等待服务器确认,这时客户端的状态为SYN_SENT。
第二次握手:当服务器收到客户端发送的SYN后,服务器要做的是确认客户端发送过来的SYN,在这里服务器发送确认包ACK,这里的ACK为x+1,意思是说“我收到了你发送的SYN了”,同时,服务器也会向客户端发送一个SYN包,这里我们设SYN为y。这时服务器的状态为SYN_RECV。 一句话,服务器端发送SYN和ACK两个包。
第三次握手:客户端收到服务器发送的SYN和ACK包后,需向服务器发送确认包ACK,“我也收到你发送的SYN了,我这就给你发个确认过去,然后我们即能合体了”,这里的ACK为y+1,发送完毕后,客户端和服务器的状态为ESTABLISH,即TCP连接成功。 在三次握手中,客户端和服务器端都发送两个包SYN和ACK,只不过服务器端的两个包是一次性发过来的,客户端的两个包是分两次发送的。
TCP数据传输过程如下图:
TCP四次分手
当A端和B端要断开连接时,需要四次交流,这里称为四次挥手。 图示如下:
这里有两个包: FIN:Finish,ACK确认序号
具体解释如下:
断开连接请求可以由客户端发出,也可以由服务器端发出,在这里我们称A端向B端请求断开连接。
第一次挥手:A端向B端请求断开连接时会向B端发送一个带有FIN标记的报文段,这里的FIN是Finish的意思。
第二次挥手:B端收到A发送的FIN后,B段现在可能现在还有数据没有传完,所以B端并不会马上向A端发送FIN,而是先发送一个确认序号ACK,意思是说“你发的断开连接请求我收到了,但是我现在还有数据没有发完,请稍等一下呗”。
第三次挥手:当B端的事情忙完了,那么此时B端就可以断开连接了,此时B端向A端发送FIN序号,意思是这次可以断开连接了。
第四次挥手:A端收到B端发送的FIN后,会向B端发送确认ACK,然后经过两个MSL时长后断开连接。 (MSL是Maximum Segment Lifetime,最大报文段生存时间,2个MSL是报文段发送和接收的最长时间)
并发服务器
并发服务器是socket应用编程中最常见的应用模型。根据连接方式分为长连接和短连接。如下:
通信方式 | 具体通信过程 |
---|---|
长连接 | 建立SOCKET连接后不管是否使用都保持连接 |
短连接 | 双方有数据交互时,建立TCP连接,数据发送完成后断开连接 |
分类:并发服务器模型根据处理方式可分为同步方式和异步方式。
同步是客户端发送请求给服务器等待服务器返回处理结果;
异步是指客户端发送请求给服务器,不等待服务器返回处理结果,而直接去完成其他的流程,对于处理结果客户端可以事后查询和让服务器进行主动通知。
单进程服务器:同⼀时刻只能为⼀个客户进⾏服务,不能同时为多个客户服务。类似于找⼀个“明星”签字⼀样,客户需要耐⼼等待才可以获取到服务。
多进程服务器
优点:通过为每个客户端创建⼀个进程的⽅式,能够同时为多个客户端进⾏服务
缺点:当客户端不是特别多的时候,这种⽅式还⾏,如果有⼏百上千个,就不可取了,因为每次创建进程等过程需要好较⼤的资源
下面我们用Python代码实现一个基于TCP的多进程服务器,代码如下:
# 实现多进程的方式:
# 1. 实例化对象
# 2. 继承子类
# 注意: 一定要确定多进程要处理的任务
# 任务: 处理客户端请求并为其服务
def dealWithClient(clientSocketObj, clientAddress):
while True:
# 5. 接收客户端发送的消息
recv_data = clientSocketObj.recv(1024).decode('utf-8')
print(clientAddress[0] + str(clientAddress[1]) + ':> ' + recv_data)
if recv_data == 'quit':
break
clientSocketObj.close()
import socket
from multiprocessing import Process
# 1. 创建服务端socket对象
server = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 2. 绑定地址和端口(IP:port)
server.bind(('0.0.0.0', 9997))
# 3. 监听是否有客户端连接?listen
server.listen(5)
print('server start .........')
while True:
# 4.接收客户端的连接accept
clientSocketObj, clientAddress = server.accept()
# dealWithClient(clientSocketObj)
p = Process(target=dealWithClient, args=(clientSocketObj, clientAddress))
p.start()
对于多进程服务器的缺点,我们可以采用多线程服务器解决,基本原理如下图:
下面用Python代码实现一个简易的HTTP服务器,HTTP是一个简单的请求-响应协议,它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。请求和响应消息的头以ASCII码形式给出;而消息内容则具有一个类似MIME的格式。具体代码如下:
import socket
def handler(clientSocketObj):
# 5. 接收客户端传递的消息
recv_data = clientSocketObj.recv(1024)
print("*"*10)
print(recv_data)
# 6. 恢复消息
clientSocketObj.send(b'HTTP/1.1 200 OK\r\n\r\n')
clientSocketObj.send(b'<h1 style="color:green">index</h1>')
def webServer():
# 1. 创建socket对象
server = socket.socket()
# 2. 绑定IP和端口
server.bind(('0.0.0.0', 8082))
# 3. 监听
server.listen(5)
print("自定义的HTTP服务8082开启.........")
while True:
# 4. 接收客户端连接
clientSocketObj, clientAddress = server.accept()
import threading
t = threading.Thread(target=handler, args=(clientSocketObj, ))
t.start()
if __name__ == '__main__':
webServer()
运行之后,在浏览器中输入我们自己的IP地址后面再用冒号连接端口号,就可以打开我们自定义的HTTP服务了,如下所示: