一、socket简介
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出的程序自然就是遵循tcp/udp标准的。
也有人将socket说成ip+port,ip是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的
一个应用程序,ip地址是配置到网卡上的,而port是应用程序开启的,ip与port的绑定就标识了互联网中独
一无二的一个应用程序
而程序的pid是同一台机器上不同进程或者线程的标识
1.1、套接字:
套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。
基于文件类型的套接字:AF_UNIX。unix一切皆文件,基于文件的套接字调用的就是底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信
基于网络类型的套接字:AF_INET。还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过,我们只需关心ipv4即可。
1.2、套接字的工作流程
套接字的工作流程主要是基于TCP(传输控制协议)的,需要双方稳定连接才可以通信。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
二、实现简单通信
先来实现一个简单的socket服务器,一次性完成收发数据。
server服务端:
import socket
server = socket.socket() # 创建服务器套接字,相当于买手机
server.bind(('0.0.0.0',8081)) # 把地址绑定到套接字
server.listen(5) # 监听链接
conn, addr = server.accept() # 待机等待接电话,返回一个(socket对象,客户端的地址信息)
# print(conn) # <socket.socket fd=112, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8081), raddr=('127.0.0.1', 52971)>
# print(addr) # ('127.0.0.1', 52971)
data = conn.recv(1024) # # 接听别人说话 只接收1024个字节 bytes
print(data)
conn.send(b'hello world!') # # 跟别人说话
conn.close() # 关闭通信连接
server.close() # 关闭服务端
client客户端:
import socket
client = socket.socket() # 创建客户端套接字
client.connect(('127.0.0.1',8081)) # 找服务器连接
client.send(b'hello Big world,I am a handsome boy!') # 发送消息
data = client.recv(1024) # 接收回信
print(data)
client.close()
以上实现了简单的通信,但是不能重复收发信息,这时,我们的while循环就派上了用场。
三、通信循环
server端:windows端会有异常,需要捕获
import socket
'''只能服务一台客户端,客户端断开后,就断开服务端的服务'''
server = socket.socket()
server.bind(('0.0.0.0',8081)) # 绑定地址
server.listen(5) # 监听
conn, addr = server.accept() # 返回一个(socket对象,客户端的地址信息)
# print(conn) # <socket.socket fd=112, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8081), raddr=('127.0.0.1', 52971)>
# print(addr) # ('127.0.0.1', 52971)
while True:
try:
data = conn.recv(1024)
print(data)
conn.send(data.upper())
except ConnectionResetError:
break
conn.close()
server.close()
client端:这里写了一个死循环,但是有input的阻塞,与服务端实现通信
import socket
client = socket.socket()
client.connect(('127.0.0.1',8081))
while True:
msg = input('>>>:').encode('utf-8')
if len(msg) == 0:continue
client.send(msg)
data = client.recv(1024)
print(data)
client.close()
但是上面的服务端只能满足一台客户端的通信,当客户端停止通信后,服务端也就关闭了连接。如果这时有客户端在排队等着访问服务端的话,就会抛异常。因此下面要实现一种服务端不停服务的功能。
四、链接通信
server端:在这里把accept()阻塞,等待连接也放在循环体内,实现一次通信结束后,可以开始下一次通信。
import socket
server = socket.socket()
server.bind(('0.0.0.0',8081)) # 绑定地址
server.listen(5) # 监听,半连接池
while True:
conn,addr = server.accept() # 阻塞
while True:
try:
data = conn.recv(1024) # 阻塞
if len(data) == 0:break # 针对linux和mac系统 客户端异常断开反复收空的情况
print(data)
conn.send(data.upper())
except ConnectionResetError:
break
conn.close()
server.close()
client端:客户端没有什么改变
import socket
client = socket.socket()
client.connect(('127.0.0.1',8081))
while True:
msg = input('>>>:').encode('utf-8')
if len(msg) == 0:continue
client.send(msg)
data = client.recv(1024)
print(data)
client.close()
五、TCP传输的特点
server端
import socket
server = socket.socket()
server.bind(('127.0.0.1',8088))
server.listen(5) # 半连接池
conn,addr = server.accept()
data = conn.recv(4)
print(data)
data = conn.recv(5)
print(data)
data = conn.recv(5)
print(data)
根据输出结果,可以发现至少有三种情况:
第一种就是一次性全发,剩下的为空
第二种正常收发
第三种:
由上我们知道,基于TCP协议传输会将数据量比较小的并且时间间隔比较短的数据一次性打包发送给接收端
client端
import socket
client = socket.socket()
client.connect(('127.0.0.1',8088))
client.send(b'hello')
client.send(b'hello')
client.send(b'hello')
六、粘包及解决
只有TCP有粘包现象,UDP永远不会粘包
发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。
发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。
解决粘包的最终方案:
服务端
1.先发报头
2.再发字典
3.再发你的真实数据
客户端
1.先收4个长度的报头
2.解包拿到字典数据长度
3.接收字典(反序列化) 》》》 获取字典里面所有信息
4.接收真实数据
在这里用到一个struct模块,它可以将一个类型,如数字,转成固定长度的bytes,在本次代码中使用 i 模式,它可以将我们传输的二进制字符串的长度转成固定4位,使得我们的接收端可以接收固定长度。
关于struct:http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html
server端:
import socket
import subprocess
import struct
import json
'''
服务端:
要有固定的ip和port
24小时不间断提供服务
'''
server = socket.socket()
server.bind(('0.0.0.0',8088))
server.listen(5) # 半连接池
while True:
conn,addr = server.accept() # 阻塞
while True:
try:
data = conn.recv(1024).decode('utf-8') # 阻塞,接收客户端传来的命令
if len(data) == 0:break # 针对linux和mac系统 客户端异常断开反复收空的情况
obj = subprocess.Popen(
data,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
# 命令执行结果
stdout = obj.stdout.read()
stderr = obj.stderr.read()
print('命令执行结果大小:', len(stdout + stderr))
# 为避免粘包,必须自定制报头
header_dic = {
'filename': 'cls.avi',
'len': len(stdout + stderr)
}
# 为了该报头能传送,需要序列化并且转为bytes
header_bytes = json.dumps(header_dic).encode('utf-8')
# 为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节
header = struct.pack('i', len(header_bytes))
# 发送报头的长度
conn.send(header)
# 发送报头的字节格式
conn.send(header_bytes)
# 发送真实数据
conn.send(stderr + stdout)
except ConnectionResetError:
break
conn.close()
server.close()
client端:
import socket
import struct
import json
client = socket.socket()
client.connect(('127.0.0.1',8088))
while True:
cmd = input('请输入指令:').encode('utf-8')
if len(cmd) == 0: continue
client.send(cmd)
# 先收报头4个bytes,得到报头长度的字节格式
header = client.recv(4)
# 对报头进行解包,获取真实数据的长度
head_len = struct.unpack('i', header)[0]
head_dic = json.loads(client.recv(head_len).decode('utf-8'))
print('报头字典:', head_dic)
# 对要接收的数据,进行循环接收
total_size = head_dic['len']
recv_size = 0
# 接收结果
res = b''
while recv_size < total_size:
data = client.recv(1024)
res += data
recv_size += len(data)
print(res.decode('gbk'))
七、TCP实现大文件上传
服务端:
# -*- coding: utf-8 -*-
# @Author : Sunhaojie
# @Time : 2019/5/5 14:28
import socket
import json
import struct
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
conn, addr = server.accept()
while True:
try:
# 先接收报头
header = conn.recv(4)
# 解析报头,获取字典长度
header_len = struct.unpack('i', header)[0]
# 接收字典
header_bytes = conn.recv(header_len)
header_dic = json.loads(header_bytes.decode('utf-8'))
# 循环接收文件,存储到本地
file_size = header_dic.get('file_size')
file_name = header_dic.get('file_name')
recv_size = 0
with open(file_name, 'wb') as f:
# 循环接收文件数据
while recv_size < file_size:
data = conn.recv(1024)
f.write(data)
recv_size += len(data)
print(header_dic.get('msg'))
except ConnectionResetError:
break
conn.close()
客户端:
# -*- coding: utf-8 -*-
# @Author : Sunhaojie
# @Time : 2019/5/5 14:29
import socket
import json
import struct
import os
client = socket.socket()
client.connect(('127.0.0.1', 8080))
# 文件大小
file_size = os.path.getsize(r'G:\脱产7期\day33\udp\udp_server.py')
# 重命名文件名
file_name = 'udp服务端'
# 定义一个字典
d = {
'file_size': file_size,
'file_name': file_name,
'msg': '发送完了啊!'
}
data_bytes = json.dumps(d).encode('utf-8')
# 制作报头
header = struct.pack('i', len(data_bytes))
# 发送报头
client.send(header)
# 发送字典
client.send(data_bytes)
# 发送真实数据
with open(r'G:\脱产7期\day33\udp\udp_server.py', 'rb') as f:
for line in f:
client.send(line)
八、UDP
udp是数据报协议,相对于TCP来说,它是一种不可靠的传输协议,因为他没有开始建立连接的三次挥手,客户端会直接发送文件,不管服务端是否在收。
udp的特点:
1.UDP协议不存在粘包问题
2.客户端可以发空
3.udp可以实现并发的效果
4.服务端不存在,也不影响客户端朝服务端发送数据
udp服务端
import socket
server = socket.socket(type=socket.SOCK_DGRAM)
server.bind(('127.0.0.1', 8081))
while True:
data, addr = server.recvfrom(1024)
print(data)
server.sendto(data.upper(), addr)
udp客户端
import socket
client = socket.socket(type=socket.SOCK_DGRAM)
# 服务端地址,通常写在配置文件中
server_addr = ('127.0.0.1', 8081)
while True:
client.sendto(b'hello baby', server_addr)
msg, addr = client.recvfrom(1024)
print(msg)
案例:用udp实现简易版的即时通信
服务端:
import socket
server = socket.socket(type=socket.SOCK_DGRAM)
server.bind(('127.0.0.1', 8082))
while True:
msg, addr = server.recvfrom(1024)
print(msg.decode('utf-8'))
data = input(">>:").encode('utf-8')
server.sendto(data, addr)
客户端:
import socket
client = socket.socket(type=socket.SOCK_DGRAM)
server_addr = ('127.0.0.1', 8082)
while True:
cmd = input(">>:").encode('utf-8')
cmd = '来自客户端1的消息:%s' % cmd
client.sendto(cmd, server_addr)
msg, addr = client.recvfrom(1024)
print(msg.decode('utf-8'))
九、SocketServer模块
1.能够实现并发效果
并发:看起来像同时运行就能称之为并发
2.udp在使用的时候,多个客户端要有一些io操作
不然容易卡死
9.1、TCP实现并行
服务端:
import socketserver
class MyServer(socketserver.BaseRequestHandler):
def handle(self):
# 通信循环
while True:
data = self.request.recv(1024)
print(data)
self.request.send(data.upper())
if __name__ == '__main__':
server = socketserver.ThreadingTCPServer(('127.0.0.1', 8080), MyServer)
server.serve_forever()
客户端:
import socket
import time
client = socket.socket()
client.connect(('127.0.0.1',8080))
while True:
client.send(b'hello')
data = client.recv(1024)
print(data)
time.sleep(0.5)
9.2、UDP实现并行
服务端:
import socketserver
class MyServer(socketserver.BaseRequestHandler):
def handle(self):
while True:
# self.request相当于你的conn通信对象
msg, sock = self.request
print(msg)
sock.sendto(msg.upper(), self.client_address) # self.client_address客户端地址
if __name__ == '__main__':
server = socketserver.ThreadingUDPServer(('127.0.0.1', 8080), MyServer)
server.serve_forever()
server.server_close()
客户端:
import socket
import time
client = socket.socket(type=socket.SOCK_DGRAM)
server_addr = ('127.0.0.1',8080)
while True:
client.sendto(b'hello', server_addr)
data, addr = client.recvfrom(1024)
print(data, addr)
time.sleep(1)