socket的使用
Posted lynn578
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了socket的使用相关的知识,希望对你有一定的参考价值。
一、socket的概念和黏包现象
1. socket的概念
socket是介于应用层和网络各个协议族通信之间的抽象层。socket将底层复杂的网络协议和与目标设备通信的操作封装为一系列接口。实现应用层脱离网咯协议层,使用户直接面向socket编程。socket的类型有流式的socket、数据报的socket和原始的socket。流式的socket是基于TCP协议的,它被广泛应用于大型的、需要安全性保障数据的传输。数据报的socket是基于UDP协议的,它是一个不可靠的、无连接协议,经常被应用于不需要TCP的排序的、以速度换取安全和准确性的,以及不需要流量控制功能的应用程序,比如传输语音和影像报文等。
2. TCP黏包
发送端每次向接收端发送的数据都会存储在接收端的缓冲区,如果接收端对缓冲区的数据的读取不恰当就会导致黏包现象,所谓黏包现象的意思是说,缓冲区的数据没有被一次性读完,导致本次数据的读取缺失和下一次读取时会附上上次未读取完的数据。对于TCP协议而言,接收端对接收的数据量是不可见的,不知道一条信息有多少字节。当接收端所能容纳的数据量很大时,就能一次性读取缓冲区全部的数据,反之就产生黏包现象。那如何避免黏包现象呢?我们知道IP协议的数据报由报头和数据两部分组成,报头封装了消息发送端的一系列信息;而UDP协议也像IP协议类似封装了消息头,有了消息头等信息,接收端就能采取一系列措施来避免黏包现象。所以TCP协议要想避免黏包现象,用户程序员可以模仿IP协议和UDP协议,为数据封装一个消息头。不妨,我们做一个简单的、仅仅包含数据大小的消息头,然后在接收端获取消息头,通过消息头所包含的数据大小再向内存中读取数据,这样便解决了黏包现象。
二、基于TCP的socket的实现
1. 服务端的实现步骤
(1)配置socket
(2)绑定服务端本身设备IP和端口号
(3)设置链接数
(4)建立连接
(5)接收消息
(6)具体业务逻辑
(7)发送消息
(8)关闭所有连接
在建立连接和收发消息时需要使用死循环,在收发消息时需要使用异常处理机制来保证当客户端非法断开连接时服务端不受影响,任然能继续运作。服务端使用accept函数建立连接,它返回一个元组,元组的第一个元素为客户端的连接,第二个元素为客户端的地址。基于TCP协议的socket,接收消息使用的是recv函数,其参数为一次性接收数据的大小;发送消息使用的是send函数,其参数为所要发送的以字节形式的数据。
from socket import * address_family = AF_INET # 协议族 socket_type = SOCK_STREAM # socket类型 request_queue_size = 5 # 链接数 buffer_size = 1024 # 一次接收消息的容量 ip_And_port = ("127.0.0.1", 8080) # IP和端口号 tcp_server = socket(address_family,socket_type) # 配置socket tcp_server.bind(ip_And_port) # 绑定IP和端口号 tcp_server.listen(request_queue_size) # 设置链接数 while True: #链接循环 print("开始接收新的客户端链接") conn, addr = tcp_server.accept() # 建立连接 print("连接conn为:", conn) print("客户端地址:", addr) while True: #信息循环 try: data = conn.recv(buffer_size) # 接受数据 print("客户端发来的是:",data.decode("utf-8")) string = "回你一句,免得尴尬" conn.send(string.encode()) # 发送数据 except Exception: break #关闭流 conn.close() tcp_server.close()
2. 客户端的实现步骤
(1)配置socket
(2)建立与目标的IP和端口号的连接
(3)发送消息
(4)具体业务逻辑
(5)接收消息
(6)关闭所有连接
客户端,相对于服务端而言,它把绑定目标设备IP和端口号与建立连接合为一个操作。使用connect函数连接到服务端,参数为服务端的设备IP和端口号。一般而言,客户端会根据自己的需求主动向服务端发起连接,但是为了安全起见和服务器性能的考虑,现实中都是服务器先断开连接。
from socket import * address_family = AF_INET # 协议族 socket_type = SOCK_STREAM # socket类型 buffer_size = 1024 # 一次接收消息的容量 ip_And_port = ("127.0.0.1", 8080) # IP和端口号 tcp_client=socket(address_family,socket_type) # 实例化socket tcp_client.connect(ip_And_port) # 建立连接 while True: # 客户端运行 msg=input(‘>>: ‘).strip() if not msg:continue #用户输入不为空时继续 tcp_client.send(msg.encode(‘utf-8‘)) #发送消息 print(‘客户端已经发送消息‘) data=tcp_client.recv(buffer_size) #接收消息 print(‘收到服务端发来的消息:‘,data.decode(‘utf-8‘)) tcp_client.close() # 关闭连接
在上面简单粗糙的代码中存在着很多问题。我们先看看服务端的问题,服务端在接收消息时如果消息为空(客户端直接按了回车键),换句话说,服务端的缓存中没有任何东西,而此时客户端任然在等待服务端的响应,这是万万不应该的。所以在客户端和服务端中都应要有对这些低级错误进行过虑的功能。在这两个程序中其实还有一个最重要的bug没有解决,那就是黏包问题。通过前面的讲解我们知道需要在发送端的消息中封装一个消息头并且提供数据的大小,当发送端发送数据时,一条数据可分为两步发送,第一次发送的是数据的大小,第二次发送的才是数据;在接收端中,接收数据时可先接收发送端发送的第一个数据,我们知道接收端的缓冲区的数据是黏在一块的,为了保证接收端接收的第一个数据一定是数据的大小,客户端和服务端应该共同约定第一个数据值大小的位数。我们可以使用struct模块下的pack函数和unpack函数将数据的大小以某种形式进行转化(一般都转化为int), pack函数是对数据的封装,而unpack函数是对数据的解封。下面将通过一个远程命令的程序对以上存在的较多问题进行简单的修改。
- 服务端
from socket import * import subprocess import struct address_family = AF_INET socket_type = SOCK_STREAM request_queue_size = 5 buffer_size = 1024 ip_And_port = ("127.0.0.1", 8080) tcp_server=socket(address_family,socket_type) tcp_server.bind(ip_And_port) tcp_server.listen(request_queue_size) while True: conn,addr=tcp_server.accept() while True: try: cmd=conn.recv(buffer_size) if not cmd:break print(‘收到客户端的命令‘,cmd) #执行命令,得到命令的运行结果cmd_res res=subprocess.Popen(cmd.decode(‘utf-8‘),shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE) err=res.stderr.read() if err: cmd_res=err else: cmd_res=res.stdout.read() if not cmd_res: cmd_res=‘执行成功‘.encode(‘gbk‘) length=len(cmd_res) #将length以int的形式封装在struct中 data_length=struct.pack(‘i‘,length) conn.send(data_length) conn.send(cmd_res) print("信息发送完毕") except Exception as e: print(e) break from socket import * import subprocess import struct address_family = AF_INET socket_type = SOCK_STREAM request_queue_size = 5 buffer_size = 1024 ip_And_port = ("127.0.0.1", 8080) tcp_server=socket(address_family,socket_type) tcp_server.bind(ip_And_port) tcp_server.listen(request_queue_size) while True: conn,addr=tcp_server.accept() while True: try: cmd=conn.recv(buffer_size) if not cmd:break print(‘收到客户端的命令‘,cmd) #执行命令,得到命令的运行结果cmd_res res=subprocess.Popen(cmd.decode(‘utf-8‘),shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE) err=res.stderr.read() if err: cmd_res=err else: cmd_res=res.stdout.read() if not cmd_res: cmd_res=‘执行成功‘.encode(‘gbk‘) length=len(cmd_res) #将length以int的形式封装在struct中 data_length=struct.pack(‘i‘,length) conn.send(data_length) conn.send(cmd_res) print("信息发送完毕") except Exception as e: print(e) break
- 客户端
from socket import * import struct from functools import partial address_family = AF_INET socket_type = SOCK_STREAM # socket类型 buffer_size = 1024 # 一次接收消息的容量 ip_And_port = ("127.0.0.1", 8080) # IP和端口号 tcp_client=socket(address_family,socket_type) tcp_client.connect(ip_And_port) while True: cmd=input(‘>>: ‘).strip() if not cmd:continue if cmd == ‘quit‘:break tcp_client.send(cmd.encode(‘utf-8‘)) length_data=tcp_client.recv(4) length=struct.unpack(‘i‘,length_data)[0] #取出缓冲区接收的所有数据,并将其存放在迭代器中 myiter=iter(partial(tcp_client.recv, buffer_size), b‘‘) for i in myiter: print(i.decode("gbk")) tcp_client.close()
三、基于UDP的socket的实现
1. 服务端的实现步骤
(1)配置socket
(2)绑定IP和端口号
(3)接收数据
(4)发送数据包(需要指明目标IP和端口号)
(5)关闭所有流
与TCP协议不同的是,UDP协议不会发生黏包现象。虽然不会发生黏包现象,但它是无连接的、不安全的协议。比如当客户端发送消息给服务端时,客户端并不知道它所发送的消息是否达到服务端。而且它和TCP协议有点相似的地方在于接收端在接收数据时也是不知道数据的大小。
from socket import * address_family = AF_INET socket_type = SOCK_DGRAM ip_And_port = ("127.0.0.1", 8080) buffer_size=1024 udp_server=socket(address_family, socket_type) udp_server.bind(ip_And_port) while True: data, addr = udp_server.recvfrom(buffer_size) string = "Welcome to hear" udp_server.sendto(string.encode("gbk"), addr) print("信息发送成功")
2. 客户端的实现步骤
(1)配置socket
(2)发送数据包(需要指明目标IP和端口号)
(3)接收数据
(4)关闭所有流
不需要建立连接,只是需要在使用sendto函数时以元组的形式指明目标设备即可。
from socket import * address_family = AF_INET socket_type = SOCK_DGRAM ip_And_port = ("127.0.0.1", 8080) buffer_size=1024 udp_client=socket(address_family,socket_type) while True: msg=input(‘>>: ‘).strip() udp_client.sendto(msg.encode(‘utf-8‘), ip_And_port) data, addr = udp_client.recvfrom(buffer_size) print(data.decode(‘gbk‘))
四、socket并发
基于TCP的socket只能实现一对一服务。比如当一个客户端与服务端在进行通信时,另一个客户端此时只能处于等待状态,直到服务端结束当前通信开始下一轮通信。如果想实现socket的并发编程,我们可以使用socketserver模块。该模块的实现原理是基于socket和线程的组合。如果对socketserver的实现原理感兴趣,可以参考socketserver模块的源码。socketserver模块分为Server类和Request类。Server类一般多用于处理连接,Request类多用于处理通信。以下分别是官方文档Server类和Request类某些具体类的继承结构图。
- 服务端
由以上的继承结构图可知,在使用并发编程定义一个新的类时需要继承socketserver模块下的BaseRequestHandler类。而继承该类需要的事是覆盖原有的handle函数,在该函数中实现数据的收发。其实你会你会发现我们的代码没有多大变化,我们仅仅只是将代码放进自定义类的handle函数,而后使用自定义的类作为参数传进socketserver模块下的ThreadingTCPServer类进行实例化而已。
import socketserver class MyServer(socketserver.BaseRequestHandler): """ 对于TCP协议来说,self.request是客户端的请求链接 对于UDP协议来说,self.request是接收的消息 """ def handle(self): print(‘conn is: ‘,self.request) print(‘addr is: ‘,self.client_address) while True: try: #收消息 data=self.request.recv(1024) if not data:break print(‘收到客户端的消息是‘,data,self.client_address) #发消息 self.request.sendall(data.upper()) except Exception as e: print(e) break #测试 if __name__ == ‘__main__‘: s=socketserver.ThreadingTCPServer((‘127.0.0.1‘,8080),MyServer) #多线程 s.serve_forever() #运行
- 客户端
from socket import * ip_port=(‘127.0.0.1‘,8080) back_log=5 buffer_size=1024 tcp_client=socket(AF_INET,SOCK_STREAM) tcp_client.connect(ip_port) while True: msg=input(‘>>: ‘).strip() if not msg:continue if msg == ‘quit‘:break tcp_client.send(msg.encode(‘utf-8‘)) data=tcp_client.recv(buffer_size) print(‘收到服务端发来的消息:‘,data.decode(‘utf-8‘)) tcp_client.close()
以上是关于socket的使用的主要内容,如果未能解决你的问题,请参考以下文章