Socket - 套接字编程
[toc]
Socket - 套接字编程
只要涉及到远程数据交互必须要操作OSI七层模型,那么每层都需要相应的程序去操作,现在就需要一个模块去操作,直接实现;
Socket是处于应用层和传输层之间的抽象层,Socket并不是一个单独的层,在我们设计程序软件的时候,它会让编程变的更简单,我们大量用的都是通过socket实现的;
Socket的作用显而易见,TCP和UDP比喻成小弟,socket是大哥,那么下面的协议(TCP/UDP)不需要我们去管,这样暴露出来的只有Socket接口,Socket自动的去组织数据,来符合指定的协议标准;
Socket 通信流程图
Socket基于
TCP/IP协议的面向连接的通信,分为客户端和服务端,必须先启动服务端,然后再启动客户端去链接服务端;
Socket模块
socket()方法
客户端和服务端的入口,默认就是基于网络的TCP协议传输;
部分参数
套接字家族:
- AF_UNIX:本机通信
- AF_INET:TCP/IP协议,使用IPV4,基于网络传输
- AF_INET6:TCP/IP协议,使用IPV6,基于网络传输
类型分类(type)
- SOCK_STREAM:TCP协议(默认采用,流式协议)
- SOCK_DGRAM:UDP协议
- SOCK_RAW:原始套接字
proto参数是协议标志,默认为0,原始套接字需要指定值
部分源码
bind()方法
绑定函数的作用就是为调用socket()函数产生的套接字分配一个本地协议地址,建立地址与套接字的对于关系;
# 源码 def bind(self, address: Union[_Address, bytes]) -> None: ... # 参数 address: Union[_Address, bytes] address:要接收的数据类型集合 ->:返回什么(返回值) # 示例bind((ip,端口)) server.bind(('127.0.0.1', 8080)) # 绑定ip和端口
listen()方法
监听函数,作用是建立半连接池,规定最大连接数,在windows系统下如果客户端数量超过半连接池规定的数量会报错;
server.listen(5) # 半连接池 # 如果服务端正在和一个客户端做交互,那么半连接池就规定了,还可以服务几个客服端; # 类似于,餐厅门口可以让顾客坐的凳子,满了就不能坐了
accept()方法
作用就是使服务器接受客户端的连接请求;
运行服务端,会在此监听,等待请求;
def accept(self) -> Tuple[socket, _RetAddress]: ... # 返回一个元组,元组包括socket对象和地址信息 """ accept() -> (socket object, address info) Wait for an incoming connection. Return a new socket representing the connection, and the address of the client. For IP sockets, the address info is a pair (hostaddr, port). """
accept()函数返回值:sock、addr
sock
:用于操作服务端和客户端连接的双向通道的媒介addr
:客户端的地址sock.recv()
:接收消息,返回bytes类型数据
def recv(self, bufsize: int, flags: int = ...) -> bytes: ... # 示例 sock.recv(1024) # 接收客户端发送的消息,一次接收1024bytes
sock.send()
:发送消息,返回int类型数据
def send(self, data: bytes, flags: int = ...) -> int: ... # 示例 sock.send(b'HammerZe')
connect() 方法
作用是TCP客户端连接服务器
def connect(self, address: Union[_Address, bytes]) -> None: ... # 示例 # 格式:connect((ip,port)),里面是tuple类型 client.connect(('127.0.0.1', 8080))
close()方法
关闭套接字,并立即返回到进程;
sock.close() server.close()
服务端客户端对比
编写小妙招:
服务端 | 客户端 | 对比 |
---|---|---|
server=socket.socket() | client=socket.socket() | 开头必须 |
server.bind((ip,port)) | client.connect((ip,port)) | 连接必须 |
server.listen(n) # 半连接池 | 服务端 | |
sock,addr = server.accept() # 获取客户端请求,返回sock,addr(客户端地址) |
服务端 | |
sock.recv(1024) # 服务端接收内容,1024为size |
client.send(bytes) # 客户端发送bytes类型数据 |
服务端和客户端相对 |
sock.send(bytes) # 服务端发送bytes类型数据 |
client.recv(1024) # 客户端接收内容,1024为size |
服务端和客户端相对 |
sock.close() # 关闭会话通道,断开连接 |
服务端 | |
server.close() # 关闭套接字 |
client.close() # 关闭套接字 |
服务端和客户端 |
注意:服务端和客户端不可同时发数据(send),也不可同时收数据(recv)
简单案例
服务端
'''server.py''' import socket server = socket.socket() # 建立连接 server.bind(('127.0.0.1', 8085)) # 半连接池 server.listen(5) # 获取客户端请求 sock, address = server.accept() print(address) # 接收数据 data = sock.recv(1024) print(data) # 发送数据 sock.send(b"Hi,This is server-side!") # 关闭通话 sock.close() # 关闭套接字 server.close()
客户端
'''client.py''' import socket client = socket.socket() # 建立连接 client.connect(('127.0.0.1', 8085)) # 发送数据 client.send(b"Hello guys,I'am HammerZe!") # 接收数据 data = client.recv(1024) print(data) # 关闭套接字 client.close()
简易通信循环
光发一条消息不够过瘾是吧,如何通信循环,你侬我侬,如下:
服务端
import socket server = socket.socket() # 建立链接 server.bind(('127.1.1.1', 8086)) # 半连接池 server.listen(5) # 监听 sock,address = server.accept() while True: # 接收 data = sock.recv(1024) print(data.decode('utf8')) # 发送 to_cmsg = input('请输入发给客户端的内容>>>:').strip() sock.send(to_cmsg.encode('utf8'))
客户端
import socket client = socket.socket() # 获取ip和端口 client.connect(('127.1.1.1', 8086)) # 发送数据 while True: to_smsg = input('请输入发给服务端的内容>>>:').strip() client.send(to_smsg.encode('utf8')) # 接收数据 data = client.recv(1024) print(data.decode('utf8'))
优化通信循环
优化内容:
- 解决,发空消息会停住,导致双方接收和发送混乱
- 解决Address already in use 报错(端口占用错误)
- 解决服务端启用,客户端主动重启报错,错误内容:远程主机强迫关闭了一个现有的连接,原因是没有走三次握手和四次挥手主动断开;
服务端
import socket from socket import SOL_SOCKET,SO_REUSEADDR server = socket.socket() # 解决断开占用报错 server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加 # 建立链接 server.bind(('127.1.1.1', 8086)) # 半连接池 server.listen(5) # 监听 while True: sock,address = server.accept() while True: try: # 接收 data = sock.recv(1024) if len(data) ==0:continue print(data.decode('utf8')) to_cmsg = data+ b'server' sock.send(to_cmsg) except Exception: break
客户端
import socket client = socket.socket() # 获取ip和端口 client.connect(('127.1.1.1', 8086)) # 发送数据 while True: to_smsg = input('请输入发给服务端的内容>>>:').strip() if not to_smsg: print('不能输入空消息,请重新输入') continue client.send(to_smsg.encode('utf8')) # 接收数据 data = client.recv(1024) print(data.decode('utf8'))
黏包问题
数据管道的数据没有被完全取出;TCP特性导致黏包,当数据量比较小 且时间间隔比较短,交互多次数据,那么TCP会自动打包成一个数据包发送;
情景一:如果交互的数据比规定接收的字节大,那么只会接收规定的字节大小,那么下次通信,继续传输上次没有传完的数据(互通管道,先进先出,TCP流式协议);
情景二:如果交互的数据太小,比如想交互三次发三次hello,那么TCP会一次发完;
- 解决办法:调整规定接收size,调大或调小(不推荐)
- 使用Struct规定固定报头(推荐)
Struct 模块
使用Struct模块规定了报头的长度,通过服务端定制报头和客户端解析报头来获取真实数据的长度,从而接收真实的数据内容,解决黏包问题;
规定报头
案例
import struct import json info_dic = { 'name':'Hammer', 'age':18, } # 序列化 json_info_dic = json.dumps(info_dic) # 真实长度 print(len(json_info_dic)) # 29 # 定制报头 herder = struct.pack('i',len(json_info_dic)) print(len(herder)) # 4 # 解析报头 parse_herder = struct.unpack('i',herder)[0] print(parse_herder) # 29
解决黏包问题
客户端
import socket import subprocess import json import struct server = socket.socket() server.bind(('127.1.1.2', 8087)) server.listen(5) while True: sock, address = server.accept() while True: data = sock.recv(1024) # 接收cmd命令 command_cmd = data.decode('utf8') sub = subprocess.Popen(command_cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) res = sub.stdout.read() + sub.stderr.read() # 结果可能很大 # 1.制作报头 data_first = struct.pack('i', len(res)) # 2.发送报头 sock.send(data_first) # 3.发送真实数据 sock.send(res)
服务端
import socket import struct client = socket.socket() client.connect(('127.1.1.2', 8087)) while True: msg = input('请输入cmd命令>>>:').strip() if len(msg) == 0: continue client.send(msg.encode('utf8')) # 1.先接收固定长度为4的报头数据 recv_first = client.recv(4) # 2.解析报头 real_length = struct.unpack('i',recv_first)[0] # 3.接收真实数据 real_data = client.recv(real_length) print(real_data.decode('gbk'))
上传文件案例
客户端
import json import os import socket import struct client = socket.socket() client.connect(('127.0.0.1', 8080)) while True: data_path = r'C:\Users\32972\Desktop\python课外题目' # print(os.listdir(data_path)) # ['python练习题.md', 'Python阶段面试题2019-2-15.docx'] file_list = os.listdir(data_path) for index, value in enumerate(file_list, 1): print(index, value) # 0 python练习题.md 1 Python阶段面试题2019-2-15.docx file_choice = input('请输入想要上传的文件编号>>>>').strip() if not file_choice: print('上传文件编号不能为空') continue if not file_choice.isdigit(): print('文件编号必须是纯数字') continue file_choice = int(file_choice) if file_choice not in range(1, len(file_list) + 1): print('没有该文件') continue # 获取文件名称 file_name = file_list[file_choice - 1] print(file_name) # 拼接路径 real_file_path = os.path.join(data_path, file_name) # print(real_file_path) # C:\Users\32972\Desktop\python课外题目\python练习题.md # 优化操作 data_dict = { 'file_name': file_name, 'desc': 'python面试题', 'size': os.path.getsize(real_file_path), 'info': '通关面试必备!' } data_json = json.dumps(data_dict) # 制作字典报头 data_first = struct.pack('i', len(data_json)) # 发送字典报头 client.send(data_first) # 发送字典 client.send(data_json.encode('utf8')) # 发送真实数据 with open(real_file_path, 'rb') as f: for line in f: client.send(line)
服务端
import json import socket import struct server = socket.socket() server.bind(('127.0.0.1',8080)) server.listen(5) while True: sock,address = server.accept() while True: # 接收报头 recv_first = sock.recv(4) # 解析字典报头 dict_length = struct.unpack('i',recv_first)[0] # 接收字典数据 real_data = sock.recv(dict_length) # 解析字典,返序列化 real_dict = json.loads(real_data) print(real_dict) # 获取字典中的内容 data_size = real_dict.get('size') file_name = real_dict.get('file_name') # 接收 recv_size = 0 with open(file_name,'wb') as f: while recv_size<data_size: data = sock.recv(1024) recv_size += len(data) f.write(data)
UDP通信
import socket udp_sk = socket.socket(type=socket.SOCK_DGRAM) # UDP协议 udp_sk.bind(('127.0.0.1',9000)) # 绑定地址 msg,addr = udp_sk.recvfrom(1024) udp_sk.sendto(b'hi',addr) udp_sk.close() import socket ip_port=('127.0.0.1',9000) udp_sk=socket.socket(type=socket.SOCK_DGRAM) udp_sk.sendto(b'hello',ip_port) back_msg,addr=udp_sk.recvfrom(1024) print(back_msg.decode('utf-8'),addr)
- python socket网络编程步骤详解(socket套接字使用)
- linux网络编程之socket(十五):UNIX域套接字编程和socketpair 函数
- 基于TCP协议的socket套接字编程
- 【网络编程6】Java与C语言套接字Socket通信的例子
- Socket编程实践(9) --套接字IO超时设置方法
- C#2.0 Socket套接字编程之实例初探
- windows编程之socket套接字2
- UNIX环境高级编程学习之第十六章网络IPC:套接字 - 简单UDP Socket 通信
- Java套接字Socket编程--TCP参数
- C#2.0 Socket套接字编程之实例初探
- C/C++ socket编程教程之四:使用socket()函数创建套接字
- Java套接字(Socket)网络编程入门
- 基于UDP协议的socket套接字编程
- C#2.0 Socket套接字编程之实例初探
- socket网络编程中常用的SO_KEEPALIVE套接字选项
- 记一次C# UDP 编程之 “System.Net.Sockets.SocketException:“通常每个套接字地址(协议/网络地址/端口)只允许使用一次
- C#2.0 Socket套接字编程之实例初探
- C#2.0 Socket套接字编程之实例初探
- Java网络编程(19):套接字(Socket)的异常
- 基于 TCP (面向连接)和无连接UDP协议的 socket 套接字编程