python之socket编程

一、socket简介

socket(套接字)是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口,将复杂的TCP/IP协议族隐藏在接口后面,让socket去组织数据以符合指定的协议。

如下左图为socket在tcp/ip协议中的角色,右图为socket的工作流程。

    

 

 二、socket分类

套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。 

基于文件类型的套接字家族:AF_UNIX

unix一切皆文件,基于文件的套接字调用底层的文件系统来取数据,两个套接字进程运行在同一机器,可以通过访问同一个文件系统间接完成通信

基于网络类型的套接字家族:AF_INET

还有AF_INET6被用于ipv6,还有一些其他的地址家族,不过他们要么是只用于某个平台,要么就是已经被废弃,或者是很少被使用,或者是根本没有实现,所有地址家族中,AF_INET是使用最广泛的一个。python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候只使用AF_INET

三、基于TCP的socket

#server端
from socket import *
phone = socket(AF_INET,SOCK_STREAM)  #创建socket,第一个参数指定socket家族,第二个指定类型,SOCK_STREAM为tcp,SOCK_DGRAM为UDP
phone.bind(('127.0.0.1',8000)) #socket绑定ip和端口,ip应该是本机地址
phone.listen(5)  #socket开启监听,此时触发三次握手,参数表示可以挂起的请求个数
while True:
    conn,addr = phone.accept() #接收客户端连接,阻塞直至客户端发送消息
   while True:
    try:
        msg = conn.recv(1024)  #接收客户端消息
        print('收到客户端的消息:',msg)
        conn.send(msg.upper()) #向客户端发送消息
    except Exception:
           break
    conn.close()  #关闭连接
phone.close()  #关闭socket
tcp-server端
#client端
from socket import *
phone = socket(AF_INET,SOCK_STREAM)  #创建客户端socket
phone.connect(('127.0.0.1',8000))  #socket连接服务端,ip为服务端地址
while True:
    msg = input('请输入').strip()
    phone.send(msg.encode('utf-8'))  #向服务端发送消息
    msg = phone.recv(1025)   #接收服务端消息
    print('收到服务端的消息',msg)
phone.close()  #关闭客户端socket,触发四次挥手
tcp-client端

1.基于TCP的socket的工作流程

server端流程:创建socket→绑定ip和端口→开启监听→接收连接→收/发消息→关闭连接→关闭socket

client端流程:创建socket→连接服务端→收/发消息→关闭连接

2.关于TCP的socket的一些解释说明

由于tcp是基于连接的,因此必须先启动服务端,然后再启动客户端去连接服务端。

由于socket是基于tcp/ip协议的,发送和接收消息必须是二进制数据,因此客户端需要通过encode('utf-8')去进行编码

对于服务端:

  • accept的返回值为两部分,第一部分为一个连接,第二部分为客户端的ip和端口,值如下

  <socket.socket fd=224, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8080), raddr=('127.0.0.1', 58317)>
  ('127.0.0.1', 58317)

  • 外层的while True循环是为了能够接受多个客户端的请求,否则只能建立一个连接
  • 内层的while True循环是为了能够与同一个客户端进行多次收发消息,否则只能接收和发送一次消息
  • 内层循环中的try···except异常处理,是为了防止一个客户端异常终止连接后conn失效导致服务端程序崩溃

在linux系统中,如果服务端程序关闭后再马上启动,可能会报ip地址被占用,这是因为四次挥手需要时间。可以在服务端的bind操作前增加phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1).

 

四、基于UDP的socket

#server端
from socket import *
ip_port = ('127.0.0.1',8000)
buffer_size = 1024
udp_server = socket(AF_INET,SOCK_DGRAM)
udp_server.bind(ip_port)
while True:
    msg,addr = udp_server.recvfrom(buffer_size)
    print(msg)
    udp_server.sendto(msg.upper(),addr)
udp-server端
#client端
from socket import *
ip_port = ('127.0.0.1',8000)
buffer_size = 1024
udp_client = socket(AF_INET,SOCK_DGRAM)
while True:
    msg = input('请输入-->').strip()
    udp_client.sendto(msg.encode('utf-8'),ip_port)
    msg,addr = udp_client.recvfrom(buffer_size)
    print(msg)
udp-client端

1.基于UDP的socket的工作流程

server端流程:创建socket→绑定ip和端口→收/发消息

client端流程:创建socket→收/发消息(发消息需指定服务端ip和端口)

2.关于UDP的socket的一些解释说明

对于UDP的socket,由于无连接因此无需进行监听。

基于UDP的发送和接收数据,接收需要使用recvfrom(),发送需要使用sendto('二进制数据',对方ip和端口)

tcp的socket的recv()得到的数据就是发送的字符串,udp的socket的recvfrom()得到的数据是一个元组,元组中第一个值为发送的字符串,第二个值为发送端ip和端号。

 

五、socket的粘包现象

1.tcp和udp协议发送数据的过程

TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务,收发两端(客户端和服务器端)都要有一一成对的socket。发送端为了将多个包更有效地发往接收端,使用了优化方法(Nagle算法)将多次间隔较小且数据量较小的数据合并成一个大的数据块,然后进行封包;这样接收端就难于分辨出来数据块中的界限,必须提供科学的拆包机制, 即面向流的通信是无消息保护边界的。

UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务,不会使用块的合并优化算法。由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样对于接收端来说就容易进行区分处理了,即面向消息的通信是有消息保护边界的。

总结:tcp是基于数据流的,收发消息不能为空,需要在客户端和服务端都添加空消息的处理机制防止程序卡住;而udp是基于数据报的,即便输入的是空内容(直接回车),实际也不是空消息,udp协议会封装上消息头。

2.粘包

粘包只发生在tcp协议中。由于tcp协议数据不会丢,如果一次没有接收完数据包,那么下次接收会从上次接收完的地方继续接收,并且己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包,粘包的发生有以下两种情况。

①发送端在短时间内多次发送较小数据,实际会按照优化算法合并发送

from socket import *
ip_port = ('127.0.0.1',8002)
buffer_size = 1024
back_log = 5

tcp_server = socket(AF_INET,SOCK_STREAM)
tcp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)

while True:
    conn,addr = tcp_server.accept()
    data1 = conn.recv(buffer_size)
    print('第一次接收数据',data1)
    data2 = conn.recv(buffer_size)
    print('第一次接收数据', data2)
    data3 = conn.recv(buffer_size)
    print('第一次接收数据', data3)

conn.close()
tcp_server.close()
tcp-server端
from socket import *
ip_port = ('127.0.0.1',8002)

tcp_client = socket(AF_INET,SOCK_STREAM)
tcp_client.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
tcp_client.connect(ip_port)

tcp_client.send('hello'.encode('utf-8'))
tcp_client.send('world'.encode('utf-8'))
tcp_client.send('happy'.encode('utf-8'))
tcp_client.send('python'.encode('utf-8'))
tcp_client.close()
tcp-client端

执行结果如下,可见在基于tcp的socket中,一次recv并不对应一次send,send是向自身缓冲区发送数据,recv也是从自身缓冲区获取数据,recv和send没有对应关系。

而udp协议中的recvfrom和sendto是一一对应的关系,如果超出缓冲区大小接收方直接丢弃。

②接收端一次接收的数据小于发送数据,下次接收时会从上次接收完的地方继续接收

from socket import *
ip_port = ('127.0.0.1',8002)
back_log = 5

tcp_server = socket(AF_INET,SOCK_STREAM)
tcp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)

while True:
    conn,addr = tcp_server.accept()
    data1 = conn.recv(2)
    print('第一次接收数据',data1)
    data2 = conn.recv(5)
    print('第一次接收数据', data2)
    data3 = conn.recv(6)
    print('第一次接收数据', data3)

conn.close()
tcp_server.close()
tcp-server端
from socket import *
ip_port = ('127.0.0.1',8002)

tcp_client = socket(AF_INET,SOCK_STREAM)
tcp_client.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
tcp_client.connect(ip_port)

tcp_client.send('helloworldhappypython'.encode('utf-8'))
tcp_client.close()
tcp-client端

执行结果如下,可见第二次和第三次都是在上一次接收的地方继续接收数据的。

3.解决粘包

以上发生粘包的两种情况,本质都是接收端不知道发送端发送数据的大小,导致接收时获取的数据大小与发送的不一致。因此可以在发送端发送数据时,同时将数据大小也发送过去,接收端根据这个大小去获取发送的数据。

发送数据大小的实现方法:发送端先计算出数据的大小,将这个整型数字通过struct.pack('i',l)打包成4个字节的二进制,然后发送打包后的这4个字节,再发送实际数据。在实际发送时这两部分会发生粘包一起发送。接收端先获取4个字节的,再通过struct.unpack('i',l)解包拿到实际数据的大小。

from socket import *
import subprocess,struct
ip_port = ('127.0.0.1',8001)
buffer_size = 1024
back_log = 5

tcp_server = socket(AF_INET,SOCK_STREAM)
tcp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
tcp_server.bind(ip_port)
tcp_server.listen(back_log)

while True:
    conn,addr = tcp_server.accept()
    print('新的客户端连接进入',addr)
    while True:
        try:
            cmd = conn.recv(buffer_size)
            if not cmd:break  #如果发送的命令为空,终止当前循环等待下一个连接进入
            print('收到客户端的命令为',cmd.decode('utf-8'))
            res = subprocess.Popen(cmd.decode('utf-8'),shell=True,
                             stdout = subprocess.PIPE,stdin = subprocess.PIPE,stderr = subprocess.PIPE)
            err = res.stderr.read()
            if err:
                cmd_res = err  #如果err有内容,表示命令输入错误,执行结果就为err
            else:
                cmd_res = res.stdout.read()  #如果err无内容,表示命令执行成功,执行结果为标准输出的内容
            if not cmd_res: #如果命令执行成功但是没有输出,例如cd,返回一个执行成功
                cmd_res = '执行成功'.encode('gbk')
            cmd_res_length = len(cmd_res)  #获取执行结果的长度
            pack_cmd_res = struct.pack('i',cmd_res_length)  #将长度打包成4个字节的二进制形式
            conn.send(pack_cmd_res)
            conn.send(cmd_res)  #4个字节会和实际发送数据粘包一起发送
        except Exception as e:
            print(e)
            break
解决粘包:tcp-server端
from socket import *
import struct
ip_port = ('127.0.0.1',8001)
buffer_size = 1024
back_log = 5

tcp_client = socket(AF_INET,SOCK_STREAM)
tcp_client.connect(ip_port)

while True:
    cmd = input('请输入命令:').strip()
    if not cmd:continue
    if cmd == 'quit':break
    tcp_client.send(cmd.encode('utf-8'))
    cmd_res_length = struct.unpack('i',tcp_client.recv(4))[0]  #先接受4个字节并unpack,获取执行结果的长度
    cmd_res = b''
    tmp_length = 0
    while tmp_length < cmd_res_length: #如果临时长度小于执行结果的长度,则循环接受buffer_size大小的数据
        cmd_res += tcp_client.recv(buffer_size)
        tmp_length = len(cmd_res)
    print('命令执行结果为',cmd_res.decode('gbk'))

tcp_client.close()
解决粘包:tcp-client端

 

六、tcp实现并发

1.socket实现tcp并发

由于udp无连接故可实现并发,而上面几个关于tcp的socket的例子无法实现并发,即服务端如果已经接受一个连接,其他的连接无法进入,必须在当前连接中断后才可重新建立连接。通过socketserver可实现tcp的并发。socketserver需要自定义一个继承socketserver.BaseRequestHandler的类,并在类中定义一个handle方法;通过socketserver建立多线程或多进程的连接,并通过serve_forever实现多连接。

import socketserver

class MyServer(socketserver.BaseRequestHandler):
    def handle(self):  #必须自定义一个handle()方法
        print('conn:',self.request)  #相当于accept()返回的conn
        print('addr:',self.client_address)  #相当于accept()返回的addr
        while True:
            try:
                data = self.request.recv(1024)
                print('接收到的客户端消息:',data)
                self.request.sendall(data.upper())
            except Exception as e:
                print(e)
                break

if __name__ == '__main__':
    # 实例化一个对象,建立连接
    s = socketserver.ThreadingTCPServer(('127.0.0.1',8005),MyServer)  # 多线程
    # s = socketserver.ForkingTCPServer(('127.0.0.1',8005),MyServer) 多进程,系统开销多余多线程,常用ThreadingTCPServer
    print(s.server_address)  #相当于实例化传入的第一个参数
    print(s.RequestHandlerClass)  #实例化传入的第二个参数
    print(MyServer)
    print(s.socket)   #socket对象
    s.serve_forever()
tcp实现并发:tcp_server
from socket import *

tcp_client = socket(AF_INET,SOCK_STREAM)
tcp_client.connect(('127.0.0.1',8005))

while True:
    msg = input('请输入:').strip()
    if not msg :
        continue
    elif msg == 'quit':
        break
    else:
        tcp_client.send(msg.encode('utf-8'))
        data = tcp_client.recv(1024)
        print('接收到的服务端消息:',data.decode('utf-8'))

tcp_client.close()
tcp实现并发:tcp_client

将上述tcp_client复制多份,可以发现tcp_server可同时接收多个client的请求并成功返回。

2.socket实现udp并发

import socketserver

class MyServer(socketserver.BaseRequestHandler):
    def handle(self):
        print(self.request)

        try:
            data = self.request[0]
            print('接收到的客户端消息:', data)
            self.request[1].sendto(data.upper(),self.client_address)
        except Exception as e:
            print(e)

if __name__ == '__main__':
    s = socketserver.ThreadingUDPServer(('127.0.0.1',8006),MyServer)
    s.serve_forever()
socketserver实现udp并发:udp_server
from socket import *

udp_client = socket(AF_INET,SOCK_DGRAM)
udp_client.connect(('127.0.0.1',8006))

while True:
    msg = input('请输入:').strip()
    if not msg :
        continue
    elif msg == 'quit':
        break
    else:
        udp_client.sendto(msg.encode('utf-8'),('127.0.0.1',8006))
        data,addr = udp_client.recvfrom(1024)
        print('接收到的服务端消息:',data.decode('utf-8'))

udp_client.close()
socketserver实现udp并发:udp_client

将上述udp_client复制多份,udp_server也可同时接收多个client的请求并成功返回。

3.socketserver对于tcp和udp的区别

对于tcp中自定义的类,self.request表示连接(即相当于accept()返回的conn),需要再调用recv()去接收数据

对于udp中自定义的类,self.request为一个元组,元组中的第一个元素为接收的数据,第二个元素为socket对象,即self.request[0]为接收数据,通过self.request[0].sendto('xxx',self.client_address)去发送数据

两者的self.client_address都表示客户端的ip和端口

 

posted @ 2019-06-03 11:21  Forever77  阅读(254)  评论(0)    收藏  举报