10.网络编程之socket
Posted journeyer-xsh
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了10.网络编程之socket相关的知识,希望对你有一定的参考价值。
一、什么是socket?
1.1 套接字简介
套接字(socket):最初是应用于计算机两个进程之间的通信。
两种类型的套接字:基于文件的和面向网络的
- 基于文件的套接字:UNIX套接字,套接字的一个家族,并且拥有一个“家族名字”,AF_UNIX(又名:AF_LOCAL),代表地址家族:UNIX。python用的是AF_UNIX。因为两个进程运行在同一台计算机上,所以,这是基于文件的套接字。
- 基于网络的套接字:他的家族名字是:AF_INET,代表家族地址:因特网。另一个地址家族AF_INET6用ipv6寻址。
1.2 套接字地址:主机-端口对
? 主机名和端口号类似区号和电话号码的组合,有效的端口号范围为0~~65535(尽管小于1024的端口号预留给了系统)。使用POSIX兼容系统(如:Linux,Mac OS等),可以在/etc/services文件中找到预留端口号的列表。
二、面向连接的套接字和为无连接的套接字
2.1 面向连接的套接字
? 面向连接的套接字:在通信前必须先建立一个链接,也成为虚拟电路或者流套接字。它提供序列化的、可靠的、不可重复的数据交付,没有记录边界,这表示每条消息可以拆分成多个片段,在每条消息片段都能到达的前提下,按照一定顺序组合,最后将完整的消息传递给正在等待的应用程序。
? 这是基于传输控制协议(TCP协议),创建TCP套接字,必须使用SOCKET_STREAM作为套接字类型。
2.2 无连接的套接字
? 这是数据报类型的套接字。这是一种无连接的套接字,无法保证传输过程中的顺序性、可靠性、重复性,且消息是以整体发送的。
? 这是基于用户数据报协议(UDP),创建UDP套接字,必须使用SOCKET_DGRAM作为套接字。
三、python中socket
3.1 socket()模块函数
socket(socket_family,socket_type,protocol=0)
# socket_family是AF_UNIX或AF_INET
# socket_type是SOCKET_STRRAM或SOCKET_DGRAM
# protocol通常省略,默认为0,这是与特定的地址家族相关的协议,如果是0,则系统就会根据地址格式和套接类别,自动选择一个合适的协议。
# 使用from socket import *传入参数不会报错,直接imort socket传入参数会报错,显示没有这个AF_INET这个族,因为这个AF_INET这个值在socket的名称空间里,from socket import *是把所有名字都引入当前的名称空间下
3.2 套接字对象(内置)方法
名称 | 描述 |
---|---|
服务器socket方法 | |
s.bind() | 将地址(主机名、端口号对)绑定到套接字上 |
s.listen() | 设置并启动TCP监听器 |
s.accept() | 被动接受TCP客户端的连接,一直等待直到连接到达(阻塞) |
客户端socket方法 | |
s.connect() | 主动发起TCP连接(阻塞) |
s.connect_ex() | connext()的扩展版本,以错误码形式返回问题,不是抛出一个异常 |
普通socket方法 | |
s.recv() | 接受TCP消息,recv(1024)不代表一定要收到1024个字节,而是一次最多只能收这么多。(阻塞) |
s.recv_into() | 接受TCP消息到指定的缓冲区 |
s.send() | 发送TCP消息 |
s.sendall() | 完整的发送TCP消息(本质就是循环调用send,sendall在待发数据量),待发数据量大于缓冲区的剩余空间,数据不丢失,直到调用send发完 |
s.recvfrom() | 接受UDP消息和地址(阻塞) |
s.recvfrom_into() | 接受UDP消息到指定的缓冲区 |
s.sendto() | 发送UDP消息(需要写消息和地址) |
s.close() | 关闭套接字 |
面向阻塞(锁)的socket | |
s.setblocking() | 设置套接字的阻塞或非阻塞模式 |
s.settimeout() | 设置阻塞套接字操作的超时时间 |
s.gettimeout() | 获取阻塞套接字操作超时时间 |
会造成阻塞的方法,accept,recv,recvfrom,connect
3.3 Socket中的一些参数
listen(n) # n表示允许排队个数,socket允许的最大连接数=服务器正在处理的socket连接数+排队的个数。
send不需要写地址,sendto需要写地址。
四、基于TCP的socket
写在这里,不管是服务器还是客户端,在接收消息时先解码,在发送消息时编码。
4.1 创建TCP服务器
# 伪代码
ss = scoket() # 创建服务器套接字对象
ss.bind() # 套接字与地址绑定
ss.listen() # 监听连接
inf_loop: # 服务器无限循环
sc = ss.accept() # 接受客户端连接
comm_loop: # 通信循环
cs.recv()/sc.send() # 对话(接受/发送)
cs.close() # 关闭客户端套接字
ss.close() # 关闭服务端套接字,一般不用
SocketServer模块是一个以socket为基础的高级套接字模块,支持客户端请求的线程和多进程处理。
4.2 创建TCP客户端
cs = socket() # 创建客户端套接字
cs.connect() # 尝试连接服务器
somm_loop: # 通信循环
cs.send()/cs.recv() # 对话(发送/接受)
cs.close() # 关闭客户端套接字
发送的数据相关的内容组成json,先发json的长度,再发json,json中存了接下来要发送的数据长度,再发数据。
五、基于UDP的socket
5.1 创建UDP服务器
UDP服务器除了等待连接外,不需要像TCP那样做一些额外的工作。
ss = socket() # 创建服务器套接字
ss.bind() # 绑定服务器套接字
inf_loop: # 服务器无限循环
cs = ss.recvfrom()/ss.sendto() # 关闭(接受/发送)
ss.close() # 关闭服务器套接字
OSError: [WinError 10057] 由于套接字没有连接并且(当使用一个 sendto 调用发送数据报套接字时)没有提供地址,发送或接收数据的请求没有被接受。解决:socket.socket(type=socket.SOCK_DGRAM)
5.2 创建UDP客户端
cs = socket() # 创建客户端
comm_loop: # 通信循环
cs.sento()/cs.recvfrom() # 对话(发送/接受)
cs.close() # 关闭客户端套接字
六、粘包问题
6.1 粘包问题
send()要发送的数据合并成一条发送了,产生原因:发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。还有一个机制是拆包,在发送端,因为受到网卡MTU限制,会将大的超过MTU
限制的数据,进行拆分,拆分成多个小的数据,进行传输,当传输到目标主机
的操作系统层时,会将多个小数据合并成原本的数据。
网络最大带宽限制MTU=1500字节,
导致粘包问题的本质:TCP传输的是流式传输,数据与数据之间没有边界
解决粘包:自定义协议,规定发送数据的字节大小,等于设置边界,即客户端发送一条消息,服务端接收一条消息,前提是服务端知道客户端发送的一条消息的大小,这需要客户端提前告知,需要在正式的发送消息前发送一条类似于验证消息,这条消息必须固定大小,否则服务端不知道这条消息多大,怎么接收,还是有可能发生粘包现象。
- 发送端:计算要发送的数据的长度,通过struct模块转换为固定长度的4字节,发送4个字节的长度
- 接收端:接收4个字节,在使用struct.unpack把4个字节转换成数字,这个数字就是要接收数据的长度,再更据长度接收真实的数据就不会发生粘包现象了。
6.2 自定义协议解决粘包问题
# 客户端
# 设置要发送消息的长度
num = str(len(msg))
# 前面补0填充到四个字节,由于是数字不会改变值的大小
ret = num.zfill(4) # 把ret先发送过去,这个长度是固定的4个字节
sk.send(ret.encode(‘utf-8‘))
# 服务端
# 接收上面的ret
length = conn.recv(4).decode(‘utf-8‘)
# 按照客户端发送过来的长度进行接收
msg = conn.recv(length)
6.3 使用struct模块解决粘包问题
struct模块:
import struct
num = 125478568
# 转换成4个字节
ret = struct.pack(‘i‘, num)
print(ret, len(ret)) # b‘xa8xa6zx07‘ 4
print(struct.unpack(‘i‘, ret)) # 返回的是一个元组(125478568,)
4个字节差不多可以表示1G的大小的数据。
七、SocketServer模块
SocketServer的封装度较高,但是效率比较固定,处理并发的客户端请求,只改变server端的代码,client端的代码不变。
SocketServer请求处理程序是默认行为是接收连接,获取请求,然后关闭连接。因此每次向服务端发送消息时都必须重新创建一个新的套接字。
类 | 描述 |
---|---|
BaseServer | 包含核心服务器功能和mix-in类的钩子:仅用于推导,这样不会创建这个类的实例;可以用TCPServer或UDPServer创建类的实例。 |
TCPServer/UDPServer | 基础网络同步TCP/UDP服务器 |
BaseRequestHandler | 包含处理服务请求的核心功能:仅用于推导,这样不会创建这个类的实例;可以用StreamRequestHandler或DatagramRequestHandler创建类的实例。 |
StreamRequestHandler/DatagramRequesthandler | 实现TCP/UDP服务器的服务处理器 |
7.1 TCPServer+StreamRequestHandler
- StreamRequestHandler类支持像操作文件那样操作输入套接字
- 客户端和服务端发送的消息都必须加上回车和换行符 /r/n
- SockerServer请求处理程序的默认行为是接收连接、获取请求、关闭连接。
# SocketServer服务端
from socketserver import (TCPServer as TCP, StreamRequestHandler as SRH)
from time import ctime
HOST = ‘127.0.0.1‘
PORT = 9001
ADDR = (HOST, PORT)
class MyRequestHandler(SRH):
def handle(self):
# 打印连接的客户端的地址
print(‘...connected from:‘, self.client_address)
# SocketServer发送消息一定是
结尾的,由于这里接收到客户端发来的消息就是以
结尾的,所以不需要加
data = ‘[%s] %s ‘ %(ctime(), self.rfile.readline().decode((‘utf-8‘)))
# 发送消息
self.wfile.write(data.encode(‘utf-8‘))
print(‘发送完毕‘)
tcpServ = TCP(ADDR, MyRequestHandler)
print(‘waiting for connection...‘)
tcpServ.serve_forever()
# SocketServer客户端
from socket import *
HOST = ‘127.0.0.1‘
PORT = 9001
ADDR = (HOST, PORT)
BUFSIZ = 1024
while True:
tcpCliSock = socket(AF_INET, SOCK_STREAM)
tcpCliSock.connect(ADDR)
data = input(‘>>>‘)
if not data:
break
# SocketServer发送消息一定是
结尾的
tcpCliSock.send((‘%s
‘%data).encode(‘utf-8‘))
print(‘发送完毕‘)
data = tcpCliSock.recv(BUFSIZ)
if not data:
break
print(data.strip().decode(‘utf-8‘))
tcpCliSock.close()
7.2 TCPServer+BaseRequestHandler
这与TCPServer+StreamRequestHandler的区别是:通过self.request.recv()和self.request.send()两个函数来接受和发送消息而不是self.rfile.readline和self.rfile.write()。
八、补充:基础网络协议
九、例子
9.1 TCP文件上传
# 服务端
import socket
import json
import struct
sk = socket.socket()
sk.bind((‘127.0.0.1‘, 9001))
sk.listen()
conn , addr = sk.accept()
msg_len = conn.recv(4)
# 4个字节,去掉前面的0,拿到字典的长度
dic_len = struct.unpack(‘i‘, msg_len)[0]
# 接收字典并解码
jdic = conn.recv(dic_len).decode(‘utf-8‘)
# json转换成普通字典
dic = json.loads(jdic)
print(dic)
with open(dic[‘filename‘],mode=‘wb‘)as f:
while dic[‘filesize‘]>0:
data = conn.recv(1024)
dic[‘filesize‘]-=len(data)
f.write(data)
conn.close()
sk.close()
# 客户端
import socket
import json
import struct
sk = socket.socket()
sk.bind((‘127.0.0.1‘, 9001))
sk.listen()
conn , addr = sk.accept()
msg_len = conn.recv(4)
# 4个字节,去掉前面的0,拿到字典的长度
dic_len = struct.unpack(‘i‘, msg_len)[0]
# 接收字典并解码
jdic = conn.recv(dic_len).decode(‘utf-8‘)
# json转换成普通字典
dic = json.loads(jdic)
print(dic)
with open(dic[‘filename‘],mode=‘wb‘)as f:
while dic[‘filesize‘]>0:
data = conn.recv(1024)
dic[‘filesize‘]-=len(data)
f.write(data)
conn.close()
sk.close()
9.2 验证客户端的合法性
生成随机字符串
import os
# 生成32位的随机字符串
ret = os.urandom(32)
print(ret)
# 服务端
import os
import socket
import hashlib
secret_key = b‘alex_sb‘
sk = socket.socket()
sk.bind((‘127.0.0.1‘,9001))
sk.listen()
conn,addr = sk.accept()
# 创建一个随机的字符串,bytes类型
rand = os.urandom(32)
# 发送随机字符串
conn.send(rand)
# 根据发送的字符串 + secrete key 进行摘要
sha = hashlib.sha1(secret_key)
sha.update(rand)
res = sha.hexdigest()
# 等待接收客户端的摘要结果
res_client = conn.recv(1024).decode(‘utf-8‘)
# 做比对
if res_client == res:
print(‘是合法的客户端‘)
# 如果一致,就显示是合法的客户端
# 并可以继续操作
conn.send(b‘hello‘)
else:
conn.close()
# 如果不一致,应立即关闭连接
# 客户端
import socket
import hashlib
secret_key = b‘alex_sb979‘
sk = socket.socket()
sk.connect((‘127.0.0.1‘,9001))
# 接收客户端发送的随机字符串
rand = sk.recv(32)
# 根据发送的字符串 + secret key 进行摘要
sha = hashlib.sha1(secret_key)
sha.update(rand)
res = sha.hexdigest()
# 摘要结果发送回server端
sk.send(res.encode(‘utf-8‘))
# 继续和server端进行通信
msg = sk.recv(1024)
print(msg)
以上是关于10.网络编程之socket的主要内容,如果未能解决你的问题,请参考以下文章