socket的使用

 

1、socket的概念和黏包现象

 

1. socket的概念

socket是介于应用层和网络各个协议族通讯之间的抽象层。socket将底层复杂的网络协议和与目标设备通讯的操做封装为一系列接口。实现应用层脱离网咯协议层,使用户直接面向socket编程。socket的类型有流式的socket、数据报的socket和原始的socket。流式的socket是基于TCP协议的,它被普遍应用于大型的、须要安全性保障数据的传输。数据报的socket是基于UDP协议的,它是一个不可靠的、无链接协议,常常被应用于不须要TCP的排序的、以速度换取安全和准确性的,以及不须要流量控制功能的应用程序,好比传输语音和影像报文等。程序员

 

2. TCP黏包

发送端每次向接收端发送的数据都会存储在接收端的缓冲区,若是接收端对缓冲区的数据的读取不恰当就会致使黏包现象,所谓黏包现象的意思是说,缓冲区的数据没有被一次性读完,致使本次数据的读取缺失和下一次读取时会附上上次未读取完的数据。对于TCP协议而言,接收端对接收的数据量是不可见的,不知道一条信息有多少字节。当接收端所能容纳的数据量很大时,就能一次性读取缓冲区所有的数据,反之就产生黏包现象。那如何避免黏包现象呢?咱们知道IP协议的数据报由报头和数据两部分组成,报头封装了消息发送端的一系列信息;而UDP协议也像IP协议相似封装了消息头,有了消息头等信息,接收端就能采起一系列措施来避免黏包现象。因此TCP协议要想避免黏包现象,用户程序员能够模仿IP协议和UDP协议,为数据封装一个消息头。不妨,咱们作一个简单的、仅仅包含数据大小的消息头,而后在接收端获取消息头,经过消息头所包含的数据大小再向内存中读取数据,这样便解决了黏包现象。shell

 

2、基于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
View Code
  • 客户端
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()
View Code

 

 

3、基于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)关闭全部流socket

        不须要创建链接,只是须要在使用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'))

 

 

 

4、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() #运行
View Code

 

  • 客户端
    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()
View Code
相关文章
相关标签/搜索