目录
一、OSI七层参考模型和TCP/IP四层参考模型
TCP/IP(Transmission Control Protocol/Internet Protocol-传输控制协议/网间协议)
定义了主机如何接入因特网及如何在它们之间传输的标准,从字面来看,是TCP和IP的合称,但实际上是指因特网整个TCP/IP协议簇
二、网络通信三要素
1. IP地址:标识网络上一台独立的主机
2. 端口号:标识进程(应用程序)的逻辑地址,不同的进程有不同的端口号
3. 传输协议:通信的规则,如TCP,UDP协议
以上三点可以唯一标识网络中的一个进程,标识后,就可以利用socket进行通信了。
简单介绍一下TCP和UDP
1. TCP:Transmission Control Protocol 传输控制协议
1)面向连接,传输数据之前需要建立连接,即要确保目的端能够应答后才能进行数据传输
2)可传输大量数据
3)通过三次握手完成连接,是安全可靠的传输协议
4)传输速度慢,效率低
2. UDP:User Datagram Protocol 用户数据报协议
1)面向无连接,传输数据之前源端和目的端不需要建立连接,即不管目的端是什么情况,直接进行数据传输
2)每个数据报的大小都限制在64K以内
3)面向报文的不可靠的传输协议,即发送出去的数据不一定会被接收到
4)传输速度快,效率高
三、socket通信
socket(套接字),是在应用层和传输层之间的一个抽象层,是一组接口,把复杂的操作抽象为几个简单的接口供应用层调用以实现进程在网络中通信,应用程序两端通过socket向网络发出请求或应答网络请求,就相当于通信的把手
socket通信流程
以TCP为例
服务端流程:
1. 服务端创建socket:socket.socket(地址类型,socket类型,协议)
地址类型:有socket.AF_INET(IPv4,默认), socket.AF_INET6(IPv6)等
socket类型:有socket.SOCK_STREAM(TCP,默认), socket.SOCK_DGRAM(UDP)等
协议:默认为0,表示适用于所有socket类型
所以如果是IPv4,TCP,可以不加任何参数
2. 服务器为socket绑定IP地址和端口号: bind((ip地址, 端口号)),注意参数是以元组的形式传递的
3. 服务器socket监听端口号请求,随时准备接收客户端发来的连接,这时候服务器的socket并没有被打开: listen(最大连接数)-参数可以省略,代表没有限制
4. 服务器socket进入阻塞状态,所谓阻塞即accept()方法一直等到客户端返回连接信息后才返回一个新的socket,最初建立的socket开始接收下一个客户端连接请求
5. 服务器accept方法返回,连接成功,可以开始与客户端通信
6. 服务端读取信息: recv()
7. 服务器向socket写入信息: send()/sendall()
8. 服务器关闭(在现实中,服务器一般不会关闭): close()
客户端流程:
1. 客户端创建socket: socket.socket()
2. 客户端打开socket,根据服务器IP地址和端口号试图连接服务器socket: connect((ip地址, 端口号)),与服务端的一致
3. 客户端连接成功,向服务器发送连接状态信息:
4. 客户端向socket写入信息: send()/sendall()
5. 客户端读取信息: recv()
6. 客户端关闭: close()
以银行和客户为例,服务端相当于银行(socket.socket()),客户端相当于来银行办理业务的客户(socket.socket()),ip和端口号相当于银行地址(bind()),大堂经理一直等待客户到来(listen()),客户来办理业务(connect()),柜台等到了客户(accept()),办理业务(send(),recv()),业务结束,客户离开(close()),柜台完成业务(close()),银行关门(close())
一次数据传输
服务端代码:
import socket #加载socket模块
#创建socket对象
sk = socket.socket()
#绑定IP地址和端口号
sk.bind(("127.0.0.1", 13000)) #用元组进行传参 127.0.0.1 环回地址,指访问本地服务;端口号自行设置,只要是空闲的端口号就可以
#监听有没有请求
sk.listen()
print('服务端已经启动。。。')
#等待传入客户端连接,在连接成功之前,保持阻塞状态
#连接成功之后,会返回一个新的套接字和客户端的IP地址与端口号
conn, addr = sk.accept() #conn 套接字 addr IP地址与端口号
print('客户端的IP地址和端口号是:', addr)
#接收客户端发来的数据
server_recv = conn.recv(1024)
print('客户端说:', server_recv.decode('utf8')) #将byte转成字符串
server_input = input('服务端输入:') #字符串不能在网络上传输
#服务端发送数据
conn.sendall(server_input.encode('utf8')) #将字符串转成byte
#关闭socket连接
conn.close()
sk.close()
客户端代码:
import socket
#创建socket对象
sk = socket.socket()
#发起连接
sk.connect(('127.0.0.1', 13000)) #服务端的地址和端口号是定义好的,实际中集中在配置文件里
#客户端发送请求给服务端
client_input = input('客户端输入:')
sk.sendall(client_input.encode('utf8'))
#客户端接收服务端发送的数据
client_recv = sk.recv(1024) #一次接收1024个字节
print('服务端说:', client_recv.decode('utf8'))
#关闭socket连接
sk.close()
分别运行服务端和客户端代码,结果如下:
1. 运行服务端代码
2. 服务端显示:服务端已经启动。。。
3. 运行客户端代码
4. 服务端显示:客户端的IP地址和端口号是: ('127.0.0.1', 53702)
4. 客户端显示:客户端输入:你好,我想取钱
5. 服务端显示:客户端说:你好,我想取钱
6. 服务端显示:服务端输入:不好意思,你的余额不足
7. 客户端显示:服务端说:不好意思,你的余额不足
多次传输数据
上例服务端和客户端只进行了一轮对话,如果要多次对话需要加一个循环
服务端代码:
import socket #加载socket模块
#创建socket对象
sk = socket.socket()
#绑定IP地址和端口号
sk.bind(("127.0.0.1", 13000)) #用元组进行传参 127.0.0.1 环回地址,指访问本地服务;端口号自行设置,只要是空闲的端口号就可以
#监听有没有请求
sk.listen()
print('服务端已经启动。。。')
#等待传入客户端连接,在连接成功之前,保持阻塞状态
#连接成功之后,会返回一个新的套接字和客户端的IP地址与端口号
conn, addr = sk.accept() #conn 套接字 addr IP地址与端口号
##循环发送和接收数据
while True:
#接收数据
server_recv = conn.recv(1024).decode('utf8')
if server_recv == 'exit': #当输入exit时,退出循环,即终止与客户端的交互
break
print('客户端说:', server_recv) #将byte转成字符串
server_input = input('服务端输入:') #字符串不能在网络上传输
#服务端发送数据
conn.sendall(server_input.encode('utf8')) #将字符串转成byte
#关闭socket连接
conn.close()
sk.close()
客户端代码:
import socket
#创建socket对象
sk = socket.socket()
#发起连接
sk.connect(('127.0.0.1', 13000)) #服务端的地址和端口号是定义好的,实际中集中在配置文件里
#循环发送和接收数据
while True:
#客户端发送请求给服务端
client_input = input('客户端输入:')
sk.sendall(client_input.encode('utf8'))
if client_input == 'exit': #当输入exit时,退出循环,即终止与服务端的交互
break
#客户端接收服务端发送的数据
client_recv = sk.recv(1024).decode('utf8') #一次接收1024个字节
print('服务端说:', client_recv)
#关闭socket连接
sk.close()
运行结果如下:
多个客户端单独交互
如果服务端想要跟多个客户端交互,还需要在服务端再加一个循环,客户端与之前的代码类似(只是改了提示文字从“客户端”分别改成了“客户1”,“客户2”),但我们copy成两个py文件代表两个客户端
服务端代码:
import socket #加载socket模块
#创建socket对象
sk = socket.socket()
#绑定IP地址和端口号
sk.bind(("127.0.0.1", 13000)) #用元组进行传参 127.0.0.1 环回地址,指访问本地服务;端口号自行设置,只要是空闲的端口号就可以
#监听有没有请求
sk.listen()
print('服务端已经启动。。。')
#第一层循环,接收多个客户端请求
while True: #这里没有写跳出循环的条件,会一直等待客户端
conn, addr = sk.accept() #conn 套接字 addr IP地址与端口号
##循环发送和接收数据
while True: #第二层循环,与一个客户端进行多次数据收发
#接收数据
server_recv = conn.recv(1024).decode('utf8')
if server_recv == 'exit': #当输入exit时,退出循环,即终止与客户端的交互
break
print('客户端说:', server_recv) #将byte转成字符串
server_input = input('服务端输入:') #字符串不能在网络上传输
#服务端发送数据
conn.sendall(server_input.encode('utf8')) #将字符串转成byte
#关闭socket连接
conn.close()
sk.close()
运行结果如下:
服务端在两个客户退出之后一直等待:
客户1: 客户2:
多个客户端一起交互
因为上述是单线程,所以只有客户一退出后,客户二的输入才会给到服务端,如果两个客户端同时跟服务端交互,应该如何做?
1. 多线程
可以把服务端第二层循环代码写在函数里,然后用线程去调用,这样一个线程对应一个客户端,就可以多个客户端同时通信,客户端保持不变
import socket #加载socket模块
import threading #加载threading模块
def handle_client(client_socket):
while True:
# 接收数据
server_recv = client_socket.recv(1024).decode('utf8')
if server_recv == 'exit': # 当输入exit时,退出循环,即终止与客户端的交互
break
print('客户端说:', server_recv) # 将byte转成字符串
server_input = input('服务端输入:') # 字符串不能在网络上传输
# 服务端发送数据
client_socket.sendall(server_input.encode('utf8')) # 将字符串转成byte
# 关闭socket连接
client_socket.close()
#创建socket对象
sk = socket.socket()
#绑定IP地址和端口号
sk.bind(("127.0.0.1", 13000)) #用元组进行传参 127.0.0.1 环回地址,指访问本地服务;端口号自行设置,只要是空闲的端口号就可以
#监听有没有请求
sk.listen()
print('服务端已经启动。。。')
while True:
conn, addr = sk.accept() #conn 套接字 addr IP地址与端口号
thread = threading.Thread(target=handle_client, args=(conn,))
thread.start()
sk.close()
运行结果如下,可以看出在客户1没有exit时,服务端也可以与客户2进行通信:
服务端结果:
客户1: 客户2:
由于现在没有写前端的代码,所以服务端看起来无法区分客户,要加入前端代码又是比较大的工作量,这里不做说明,只是实现功能
2. socketserver
还有一种更简单的方法,加载一个新的模块socketserver
import socketserver
class MyServer(socketserver.BaseRequestHandler):
def handle(self): #重写handle方法
while True: #跟一个客户端进行多次通信
#接收数据
server_recv = self.request.recv(1024).decode('utf8')
if server_recv == 'exit':
break
print(server_recv)
#发送数据
server_input = input('服务端输入:')
self.request.sendall(server_input).encode('utf8')
self.request.close()
server = socketserver.ThreadingTCPServer(('127.0.0.1', 13000), MyServer) #完成线程和socket通信,MyServer不需要实例化,作为参数传递
server.serve_forever() #启动服务器
文件的上传与下载
例:客户端有一个名为image.PNG的图片,需要将它上传到服务端
服务端代码:
import socket
import os
def get_file(sk):
'''
接收文件
:param sk: socket对象
:return:
'''
#接收文件大小 与post_file中file_size位置对应,确保客户端的file_size是被服务端的file_size接收
file_size = int(sk.recv(1024).decode('utf8')) #decode将byte转成string,int将string转成int
sk.sendall(b'OK') #当接收到文件大小后,给客户端发送一个提示ok,与客户端post_file中的recv对应,避免粘包
#接收文件名字
file_name = sk.recv(1024).decode('utf8')
#file_size和file_name在逻辑上是不一样的,但在传输时,如果客户端发送多次的数据大小<1024,对于服务端来说会认为时一个数据,这就叫粘包
sk.sendall(b'OK') #当接收到文件大小后,给客户端发送一个提示ok,与客户端post_file中的recv对应,避免粘包
#接收文件
with open('./%s' %file_name, 'wb') as f: #打开文件,并以byte写入
while file_size > 0: #当文件大小>0,即接收到的文件还没有写入完成
f.write(sk.recv(1024)) #写入一次接收到的数据
file_size -= 1024 #完成写入一次后,需要写入的文件大小就会减少1024
#创建socket对象
sk = socket.socket()
#绑定IP地址和端口号
sk.bind(("127.0.0.1", 13002))
#监听有没有请求
sk.listen()
print('服务端已经启动。。。')
#等待传入客户端连接,在连接成功之前,保持阻塞状态
#连接成功之后,会返回一个新的套接字和客户端的IP地址与端口号
conn, addr = sk.accept()
#接收客户端发来的数据
get_file(conn)
#关闭socket连接
conn.close()
sk.close()
客户端代码:
import socket
import os
def post_file(sk, file_path):
'''
发送文件
:param sk: socket对象
:param file_path: 文件路径
:return:
'''
#发送文件大小
file_size = os.stat(file_path).st_size #获取到文件大小
sk.sendall(str(file_size).encode('utf8')) #str将int转成str类型,encode将str转成byte
sk.recv(1024) #发送完之后,加一个recv,等待服务端返回,阻塞下面的发送,避免粘包
#发送文件名字
file_name = os.path.split(file_path)[1] #将文件路径中的路径和文件名称分开,split返回一个列表,取第二个元素即文件名称
sk.sendall(file_name.encode('utf8'))
sk.recv(1024) #发送完之后,加一个recv,等待服务端返回,阻塞下面的发送,避免粘包
#发送文件内容
with open(file_path, 'rb') as f: #以二进制格式打开需要发送的文件
while file_size > 0: #当文件大小>0,即文件还没有发送完成
sk.sendall(f.read(1024)) #每次发送1024个字节
file_size -= 1024 #每次发送完成,待发送的文件大小就要减小1024
#创建socket对象
sk = socket.socket()
#发起连接
sk.connect(('127.0.0.1', 13002)) #服务端的地址和端口号是定义好的,实际中集中在配置文件里
#客户端发送请求给服务端
path = './image.PNG'
post_file(sk, path)
#关闭socket连接
sk.close()
运行结果:
运行之前的结构: 运行之后的结构: