打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
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
#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,触发四次挥手

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)
#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)

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时才会清除缓冲区内容。数据是可靠的,但是会粘包,粘包的发生有以下两种情况。

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

tcp-server端
tcp-client端

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

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

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

tcp-server端
tcp-client端

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

3.解决粘包

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

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

解决粘包:tcp-server端
解决粘包:tcp-client端

六、tcp实现并发

1.socket实现tcp并发

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

tcp实现并发:tcp_server
tcp实现并发:tcp_client

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

2.socket实现udp并发

socketserver实现udp并发:udp_server
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和端口

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
网络编程
TCP/IP UDP
【连载电子书八】Python网络编程
python TCP通信详解
从UDP的”连接性”说起
7.6.1 Socket学习网络基础准备
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服