python入门教程11-03 (python语法入门之socket通讯)

本章主要讲述Python中实现socket通讯,由于socket通讯的服务端比较复杂,并且客户端很是简单,因此客户端基本上都是用sockct模块实现,而服务端用有不少模块可使用,干货在后面,一块儿来看看吧。算法

Socket概念shell

Socket是应用层与TCP/IP协议族通讯的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来讲,一组简单的接口就是所有,让Socket去组织数据,以符合指定的协议。json

基于TCP协议的socketwindows

server端设计模式

import socket缓存

sk = socket.socket()服务器

sk.bind(('127.0.0.1',8898)) #把地址绑定到套接字sk.listen() #监听连接conn,addr = sk.accept() #接受客户端连接ret = conn.recv(1024) #接收客户端信息print(ret) #打印客户端信息conn.send(b'hi') #向客户端发送信息conn.close() #关闭客户端套接字sk.close() #关闭服务器套接字(可选)网络

client端socket

import sockettcp

sk = socket.socket() # 建立客户套接字sk.connect(('127.0.0.1',8898)) # 尝试链接服务器sk.send(b'hello!')

ret = sk.recv(1024) # 对话(发送/接收)print(ret)

sk.close() # 关闭客户套接字

问题:有的同窗在重启服务端时可能会遇到报错

#加入一条socket配置,重用ip和端口import socketfrom socket import SOL_SOCKET,SO_REUSEADDR

sk = socket.socket()

sk.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加sk.bind(('127.0.0.1',8898)) #把地址绑定到套接字sk.listen() #监听连接conn,addr = sk.accept() #接受客户端连接ret = conn.recv(1024) #接收客户端信息print(ret) #打印客户端信息conn.send(b'hi') #向客户端发送信息conn.close() #关闭客户端套接字sk.close() #关闭服务器套接字(可选)

tcp是基于连接的,必须先启动服务端,而后再启动客户端去连接服务端

![img](../配图/Mon Oct 21 2019 13:29:15 GMT+0800 (CST).png)

基于UDP协议的socket

server端

import socket

udp_sk = socket.socket(type=socket.SOCK_DGRAM) #建立一个服务器的套接字udp_sk.bind(('127.0.0.1',9000)) #绑定服务器套接字msg,addr = udp_sk.recvfrom(1024)

print(msg)

udp_sk.sendto(b'hi',addr) # 对话(接收与发送)udp_sk.close() # 关闭服务器套接字

client端

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)

udp是无连接的,启动服务以后能够直接接受消息,不须要提早创建连接

黏包现象

res=subprocess.Popen(cmd.decode('utf-8'),

shell=True,

stderr=subprocess.PIPE,

stdout=subprocess.PIPE)

输出的结果的编码是以当前所在的系统为准的,若是是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端须要用GBK解码,且只能从管道里读一次结果

同时执行多条命令以后,获得的结果极可能只有一部分,在执行其余命令的时候又接收到以前执行的另一部分结果,这种显现就是黏包

基于tcp协议实现的黏包

Server端

#_coding:utf-8_from socket import *import subprocess

ip_port=('127.0.0.1',8888)

BUFSIZE=1024tcp_socket_server=socket(AF_INET,SOCK_STREAM)

tcp_socket_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)

tcp_socket_server.bind(ip_port)

tcp_socket_server.listen(5)while True:

conn,addr=tcp_socket_server.accept()

print('客户端',addr)

while True:

    cmd=conn.recv(BUFSIZE)

    if len(cmd) == 0:break        res=subprocess.Popen(cmd.decode('utf-8'),shell=True,

                    stdout=subprocess.PIPE,

                    stdin=subprocess.PIPE,

                    stderr=subprocess.PIPE)

    stderr=res.stderr.read()

    stdout=res.stdout.read()

    conn.send(stderr)

    conn.send(stdout)

Client端

#_coding:utf-8_import socket

BUFSIZE=1024ip_port=('127.0.0.1',8888)

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

res=s.connect_ex(ip_port)while True:

msg=input('>>: ').strip()

if len(msg) == 0:continue    if msg == 'quit':break    s.send(msg.encode('utf-8'))

act_res=s.recv(BUFSIZE)

print(act_res.decode('utf-8'),end='')

黏包成因

TCP面向流的通讯特色和Nagle算法

TCP(transport control protocol,传输控制协议)是面向链接的,面向流的,提供高可靠性服务。

收发两端(客户端和服务器端)都要有一一成对的socket,所以,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将屡次间隔较小且数据量小的数据,合并成一个大的数据块,而后进行封包。

这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通讯是无消息保护边界的。

对于空消息:tcp是基于数据流的,因而收发的消息不能为空,这就须要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即使是你输入的是空内容(直接回车),也能够被发送,udp协议会帮你封装上消息头发送过去。

可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端老是在收到ack时才会清除缓冲区内容。数据是可靠的,可是会粘包。

UDP不会发生黏包

UDP(user datagram protocol,用户数据报协议)是无链接的,面向消息的,提供高效率服务。

不会使用块的合并优化算法,, 因为UDP支持的是一对多的模式,因此接收端的skbuff(套接字缓冲区)采用了链式结构来记录每个到达的UDP包,在每一个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来讲,就容易进行区分处理了。 即面向消息的通讯是有消息保护边界的。

对于空消息:tcp是基于数据流的,因而收发的消息不能为空,这就须要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即使是你输入的是空内容(直接回车),也能够被发送,udp协议会帮你封装上消息头发送过去。

不可靠不黏包的udp协议:udp的recvfrom是阻塞的,一个recvfrom(x)必须对惟一一个sendinto(y),收完了x个字节的数据就算完成,如果y;x数据就丢失,这意味着udp根本不会粘包,可是会丢数据,不可靠。

会发生黏包的两种状况

状况一 发送方的缓存机制

发送端须要等缓冲区满才发送出去,形成粘包(发送数据时间间隔很短,数据了很小,会合到一块儿,产生粘包)

状况二 接收方的缓存机制

接收方不及时接收缓冲区的包,形成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候仍是从缓冲区拿上次遗留的数据,产生粘包)

总结

黏包现象只发生在tcp协议中:

1.从表面上看,黏包问题主要是由于发送方和接收方的缓存机制、tcp协议面向流通讯的特色。

2.实际上,主要仍是由于接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所形成的

黏包解决方案

解决方案一

问题的根源在于,接收端不知道发送端将要传送的字节流的长度,因此解决粘包的方法就是围绕,如何让发送端在发送数据前,把本身将要发送的字节流总大小让接收端知晓,而后接收端来一个死循环接收完全部数据。

存在的问题:

程序的运行速度远快于网络传输速度,因此在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗

解决方案进阶

刚刚的方法,问题在于咱们咱们在发送

咱们能够借助一个模块,这个模块能够把要发送的数据长度转换成固定长度的字节。这样客户端每次接收消息以前只要先接受这个固定长度字节的内容看一看接下来要接收的信息大小,那么最终接受的数据只要达到这个值就中止,就能恰好很少很多的接收完整的数据了。

struct模块

该模块能够把一个类型,如数字,转成固定长度的bytes

import struct

obj = struct.pack('i',123456)

print(len(obj)) # 4obj = struct.pack('i',898898789)

print(len(obj)) # 4# 不管数字多大,打包后长度恒为4

使用struct解决黏包

借助struct模块,咱们知道长度数字能够被转换成一个标准大小的4字节数字。所以能够利用这个特色来预先发送数据长度。

server端 obj=subprocess.Popen(cmd.decode('utf-8'),

shell=True,

                        stdout=subprocess.PIPE,

                        stderr=subprocess.PIPE

                        )

        stdout=obj.stdout.read()

        stderr=obj.stderr.read()

        # 1. 先制做固定长度的报头            header=struct.pack('i',len(stdout) + len(stderr))

        # 2. 再发送报头            conn.send(header)

        # 3. 最后发送真实的数据            conn.send(stdout)

        conn.send(stderr)# client端 #1. 先收报头,从报头里解出数据的长度    header=client.recv(4)

total_size=struct.unpack('i',header)[0]

#2. 接收真正的数据    cmd_res=b''    recv_size=0    while recv_size < total_size:

    data=client.recv(1024)

    recv_size+=len(data)

    cmd_res+=data

print(cmd_res.decode('gbk'))

咱们还能够把报头作成字典,字典里包含将要发送的真实数据的详细信息,而后json序列化,而后用struck将序列化后的数据长度打包成4个字节(4个本身足够用了)

server端 # 1. 先制做报头 header_dic = {

'filename': 'a.txt',

    'md5': 'asdfasdf123123x1',

    'total_size': len(stdout) + len(stderr)

}

header_json = json.dumps(header_dic)

header_bytes = header_json.encode('utf-8')

# 2. 先发送4个bytes(包含报头的长度)    conn.send(struct.pack('i', len(header_bytes)))

# 3  再发送报头    conn.send(header_bytes)

# 4. 最后发送真实的数据    conn.send(stdout)

conn.send(stderr)# client端 #1. 先收4bytes,解出报头的长度    header_size=struct.unpack('i',client.recv(4))[0]

#2. 再接收报头,拿到header_dic    header_bytes=client.recv(header_size)

header_json=header_bytes.decode('utf-8')

header_dic=json.loads(header_json)

print(header_dic)

total_size=header_dic['total_size']

#3. 接收真正的数据    cmd_res=b''    recv_size=0    while recv_size < total_size:

    data=client.recv(1024)

    recv_size+=len(data)

    cmd_res+=data

print(cmd_res.decode('gbk'))

总结:先发字典报头,再发字典数据,最后发真实数据

SocketServer模块介绍

TCP socketserver使用import socketserverclassMyTcpServer(socketserver.BaseRequestHandler): defhandle(self): while True:

try:

            data = self.request.recv(1024)  # 对于tcp,self.request至关于conn对象                if len(data) == 0:break                print(data)

            self.request.send(data.upper())

        except ConnectionResetError:

            breakif __name__ == '__main__':

server = socketserver.ThreadingTCPServer(('127.0.0.1',8081),MyTcpServer)

server.serve_forever()# UDP socketserver使用import socketserverclassMyUdpServer(socketserver.BaseRequestHandler):    defhandle(self):        while True:

        data, sock = self.request

        print(data)

        sock.sendto(data.upper(), self.client_address)if __name__ == '__main__':

server = socketserver.ThreadingUDPServer(('127.0.0.1', 8080), MyUdpServer)

server.serve_forever()
相关文章
相关标签/搜索