socket编程
Posted lynnblog
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了socket编程相关的知识,希望对你有一定的参考价值。
网络编程
一、网络信息传输概述
根据计算机操作系统知识,内存分为内核态内存和用户态内存,用户应用代码运行在用户态,而系统代码(如系统服务和设备驱动)运行在内核态。正是这样的机制子保证了系统的稳定性。当发送端向接收端发送消息时,发送端发送的消息在用户态内存被操作系统调入内核态内存,然后通过网卡将消息以某种信号方式发送给接收端的网卡,接收端的网卡接收到信号以后将消息调入内核态内存,最后转向用户态内存被接收端成功接收。通过这一点,我们知道发送端和接收端发送或接收消息时都是对自己的内存做某些操作。不妨,我们打一个比方,如果发送端发送的是两条消息,那接收端接收到的是两条消息吗?答案当然是否定的。发送端发送的两条消息都会发送到接收端的内核态内存中。所以,接收端接收的消息是多少就要具体看接收端如何处理发送端发送的消息。
TCP/IP(Transmission Control Protocol / Internet Protocol)网络协议源于美国的ARPAnet工程,由它的两个协议即TCP协议和IP协议而得名。在TCP/IP这个由多个独立定义协议的集合框架中,包含了大量的协议和应用。TCP/IP的传输层提供了可靠的数据链接,以确保发送端的源主机传送数据报能正确到达目标主机。TCP作为一个面向协议的链接,它有足够好的处理机制能允许使一台机器发出的报文流差错的发往网络上的其他机器。它把输入的报文流分成多个报文段在网络上进行传输,在接受端再把报文段进行重组,同时需要进行流量控制,避免发送端发送过快而使低速的接收端无法处理的情况。IP协议是网际层的主要协议,传输的数据单元称为数据报(Datagram)。IP协议的数据报由报头和数据两部分组成。
二、socket编程
socket是介于应用层和网络各个协议族通信之间的抽象层。socket将底层复杂的网络协议和与目标设备通信的操作封装为一系列接口。实现应用层脱离网咯协议层,使用户直接面向socket编程。socket的类型有流式的socket、数据报的socket和原始的socket。流式的socket是基于TCP协议的,它被广泛应用于大型的、需要安全性保障数据的传输。数据报的socket是基于UDP协议的,它是一个不可靠的、无连接协议,经常被应用于不需要TCP的排序的、以速度换取安全和准确性的,以及不需要流量控制功能的应用程序,比如传输语音和影像报文等。
三、TCP协议的黏包现象
发送端每次向接收端发送的数据都会存储在接收端的缓冲区,如果接收端对缓冲区的数据的读取不恰当就会导致黏包现象,所谓黏包现象的意思是说,缓冲区的数据没有被一次性读完,导致本次数据的读取缺失和下一次读取时会附上上次未读取完的数据。对于TCP协议而言,接收端对接收的数据量是不可见的,不知道一条信息有多少字节。当接收端所能容纳的数据量很大时,就能一次性读取缓冲区全部的数据,反之就产生黏包现象。那如何避免黏包现象呢?我们知道IP协议的数据报由报头和数据两部分组成,报头封装了消息发送端的一系列信息;而UDP协议也像IP协议类似封装了消息头,有了消息头等信息,接收端就能采取一系列措施来避免黏包现象。所以TCP协议要想避免黏包现象,用户程序员可以模仿IP协议和UDP协议,为数据封装一个消息头。不妨,我们做一个简单的、仅仅包含数据大小的消息头,然后在接收端获取消息头,通过消息头所包含的数据大小再向内存中读取数据,这样便解决了黏包现象。
四、基于TCP的socket
1. 服务端的实现步骤
(1)配置socket
(2)绑定服务端本身设备IP和端口号
(3)设置链接数
(4)建立连接
(5)接收消息
(6)具体业务逻辑
(7)发送消息
(8)关闭所有连接
2. 客户端的实现步骤
(1)配置socket
(2)建立与目标的IP和端口号的连接
(3)发送消息
(4)具体业务逻辑
(5)接收消息
(6)关闭所有连接
服务端,在建立连接和收发消息时需要使用死循环,在收发消息时需要使用异常处理机制来保证当客户端非法断开连接时服务端不受影响,任然能继续运作。服务端使用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()
客户端,相对于服务端而言,它把绑定目标设备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 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)关闭所有流
2. 客户端的实现步骤
(1)配置socket
(2)发送数据包(需要指明目标IP和端口号)
(3)接收数据
(4)关闭所有流
服务端,与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("信息发送成功")
客户端,不需要建立连接,只是需要在使用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‘))
六、网络并发编程
基于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编程的主要内容,如果未能解决你的问题,请参考以下文章