首页 > 代码库 > python之socket编程

python之socket编程

第一、 客户端/服务器架构

C/S架构:

1.硬件C/S架构(打印机)

2.软件C/S架构(web服务)

学习socket也是为了写一款C/S架构的软件

第二、 socket介绍

前言:我们要开发一套C/S架构,首先要了解osi七层,简单说osi七层包括 : 应,表,会,传,网,数,物

主要侧重点,我们开发的基于C/S架构的一款应用程序(属于应用层),其是通过网络进行通信的,而网络的核心在于一大堆的协议,TCP/IP协议,以太网协议,如果开发一款C/S架构的软件,必须基于这些标准。现在简单介绍一下这些标准

只介绍与socket的相关的TCP/IP层:TCP/IP协议族包括运输层、网络层、链路层。

技术分享

现在你会疑问,socket在工作在那一层?详见下图:

技术分享

综合上图:

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

第三、 套接字

套接字发展史及分类:

套接字起源于 20 世纪 70 年代加利福尼亚大学伯克利分校版本的 Unix,即人们所说的 BSD Unix。 因此,有时人们也把套接字称为“伯克利套接字”或“BSD 套接字”。一开始,套接字被设计用在同 一台主机上多个应用程序之间的通讯。这也被称进程间通讯,或 IPC。套接字有两种(或者称为有两个种族),分别是基于文件型的和基于网络型的。

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

  套接字家族的名字:AF_UNIX

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

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

  套接字家族的名字:AF_INET

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

套接字工作流程:

  面向连接套接字:

技术分享

1.服务器先用 socket 函数来建立一个套接字,用这个套接字完成通信的监听。 
2.用 bind 函数来绑定一个端口号和 IP 地址。因为本地计算机可能有多个网址和 IP,每一个 IP 和端口有多个端口。需要指定一个 IP 和端口进行监听。 
3.服务器调用 listen 函数,使服务器的这个端口和 IP 处于监听状态,等待客户机的连接。 
4.客户机用 socket 函数建立一个套接字,设定远程 IP 和端口。 
5.客户机调用 connect 函数连接远程计算机指定的端口。 
6.服务器用 accept 函数来接受远程计算机的连接,建立起与客户机之间的通信。 
7.建立连接以后,客户机用 write 函数向 socket 中写入数据。也可以用 read 函数读取服务器发送来的数据。 
8.服务器用 read 函数读取客户机发送来的数据,也可以用 write 函数来发送数据。 
9.完成通信以后,用 close 函数关闭 socket 连接。

面向无连接套接字

技术分享

无连接的通信不需要建立起客户机与服务器之间的连接,因此在程序中没有建立连接的过程。进行通信之前,需要建立网络套接字。服务器需要绑定一个端口,在这个端口上监听接收到的信息。客户机需要设置远程 IP 和端口,需要传递的信息需要发送到这个 IP 和端口上。 

详解:

服务端套接字函数s.bind()    绑定(主机,端口号)到套接字s.listen()  开始TCP监听s.accept()  被动接受TCP客户的连接,(阻塞式)等待连接的到来客户端套接字函数s.connect()     主动初始化TCP服务器连接s.connect_ex()  connect()函数的扩展版本,出错时返回出错码,而不是抛出异常公共用途的套接字函数s.recv()            接收TCP数据s.send()            发送TCP数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完)s.sendall()         发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)s.recvfrom()        接收UDP数据s.sendto()          发送UDP数据s.getpeername()     连接到当前套接字的远端的地址s.getsockname()     当前套接字的地址s.getsockopt()      返回指定套接字的参数s.setsockopt()      设置指定套接字的参数s.close()           关闭套接字面向锁的套接字方法s.setblocking()     设置套接字的阻塞与非阻塞模式s.settimeout()      设置阻塞套接字操作的超时时间s.gettimeout()      得到阻塞套接字操作的超时时间面向文件的套接字的函数s.fileno()          套接字的文件描述符s.makefile()        创建一个与该套接字相关的文件

基于TCP的套接字

socket服务端:

#!/usr/bin/env python#-*-coding:utf-8-*-import socketserver=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #创建服务端套接字server.bind((127.0.0.1,8080))  #绑定ip+portserver.listen(5)           #监听连接conn,clinet_addr=server.accept()  #接受客户端连接print(conn,clinet_addr)      client_data=conn.recv(1024)      #接受客户端升级print(client data is %s%client_data)conn.send(client_data.upper())    #发送数据给客户端conn.close()    #关闭客户端套接字连接
server.close() #关闭服务端连接

socket客户端:

#!/usr/bin/env python#-*-coding:utf-8-*-import socketclient=socket.socket(socket.AF_INET,socket.SOCK_STREAM)  #创建客户端套接字client.connect((127.0.0.1,8080))      #连接服务端client.send(hello.encode(utf-8))     #发送byte格式数据client_date=client.recv(1024)      #接收服务端数据print(client_date)client.close()        #关闭连接

 

进化版:

 server端

#!/usr/bin/env python#-*-coding:utf-8-*-import socketserver=socket.socket(socket.AF_INET,socket.SOCK_STREAM)server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #如果服务端仍然存在四次挥手的time_wait状态在占用地址,需要用这个释放server.bind((127.0.0.1,8080))server.listen(5)    #backlog:最大能挂起几个连接while True: #连接循环    conn,clinet_addr=server.accept()    print(conn,clinet_addr)    while True: #通信循环        try:      #异常处理是解决,如果有一个客户端断开连接,服务端崩溃的问题            client_data=http://www.mamicode.com/conn.recv(1024)  #收多少个字节            print(client data is %s%client_data)            conn.send(client_data.upper())        except Exception:            break    conn.close()server.close()

 

客户端:

#!/usr/bin/env python#-*-coding:utf-8-*-import socketclient=socket.socket(socket.AF_INET,socket.SOCK_STREAM)client.connect((127.0.0.1,8080))while True:    msg=input(>>).strip()    if len(msg) == 0:continue    client.send(msg.encode(utf-8))    client_date=client.recv(1024) #收取1024个字节    print(client_date)client.close()

基于UDP的套接字

server端:

#!/usr/bin/env python#-*-coding:utf-8-*-import socketserver=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)server.bind((127.0.0.1,8080))while True:    data,client_addr_port=server.recvfrom(1024)    print(data.decode(utf-8))    msg=input(>>).strip()    server.sendto(msg.encode(utf-8),client_addr_port)server.close()

client:

#!/usr/bin/env python#-*-coding:utf-8-*-import socketclient=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)while True:    msg=input(>>).strip()    client.sendto(msg.encode(utf-8),(127.0.0.1,8080))    data,server_addr_port=client.recvfrom(1024)    print(data.decode(utf-8))client.close()

第四、 recv与recvfrom&&send与sendto的区别

1.声明:发消息,都是将数据发送到己端的发送缓冲中,收消息都是从己端的缓冲区中收

1.1: tcp:send发消息,recv收消息

1.2: udp:sendto发消息,recvfrom收消息

2.send与sendto

 tcp是基于数据流的,而udp是基于数据报的:

  1. send(bytes_data):发送数据流,数据流bytes_data若为空,自己这段的缓冲区也为空,操作系统不会控制tcp协议发空包
  2. sendto(bytes_data,ip_port):发送数据报,bytes_data为空,还有ip_port,所有即便是发送空的bytes_data,数据报其实也不是空的,自己这端的缓冲区收到内容,操作系统就会控制udp协议发包。
3.recv与recvfrom

1.tcp协议:

(1)如果收消息缓冲区里的数据为空,那么recv就会阻塞(阻塞很简单,就是一直在等着收)

(2)只不过tcp协议的客户端send一个空数据就是真的空数据,客户端即使有无穷个send空,也跟没有一个样。

(3)tcp基于链接通信

  • 基于链接,则需要listen(backlog),指定半连接池的大小
  • 基于链接,必须先运行的服务端,然后客户端发起链接请求
  • 对于mac系统:如果一端断开了链接,那另外一端的链接也跟着完蛋recv将不会阻塞,收到的是空(解决方法是:服务端在收消息后加上if判断,空消息就break掉通信循环)
  • 对于windows/linux系统:如果一端断开了链接,那另外一端的链接也跟着完蛋recv将不会阻塞,收到的是空(解决方法是:服务端通信循环内加异常处理,捕捉到异常后就break掉通讯循环)

2.udp协议

(1)如果如果收消息缓冲区里的数据为“空”,recvfrom也会阻塞

(2)只不过udp协议的客户端sendinto一个空数据并不是真的空数据(包含:空数据+地址信息,得到的报仍然不会为空),所以客户端只要有一个sendinto(不管是否发送空数据,都不是真的空数据),服务端就可以recvfrom到数据。

(3)udp无链接

  • 无链接,因而无需listen(backlog),更加没有什么连接池之说了
  • 无链接,udp的sendinto不用管是否有一个正在运行的服务端,可以己端一直的发消息,只不过数据丢失
  • recvfrom收的数据小于sendinto发送的数据时,在mac和linux系统上数据直接丢失,在windows系统上发送的比接收的大直接报错
  • 只有sendinto发送数据没有recvfrom收数据,数据丢失

总结:你单独运行udp的客户端,并不会报错,相反tcp却会报错,因为udp协议只负责把包发出去,对方收不收,我根本不管,而tcp是基于链接的,必须有一个服务端先运行着,客户端去跟服务端建立链接然后依托于链接才能传递消息,任何一方试图把链接摧毁都会导致对方程序的崩溃。

第五、 粘包现象

通过远程执行命令,查看粘包现象

server端

#!/usr/bin/env python#-*-coding:utf-8-*-import socketimport subprocessserver=socket.socket(socket.AF_INET,socket.SOCK_STREAM)server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #如果服务端仍然存在四次挥手的time_wait状态在占用地址,需要用这个释放server.bind((127.0.0.1,8080))server.listen(5)while True: #连接循环    conn,clinet_addr=server.accept()    print(conn,clinet_addr)    while True: #通信循环        try:      #异常处理是解决,如果有一个客户端断开连接,服务端崩溃的问题            cmd=conn.recv(1024)            if not cmd:break            cmd=cmd.decode(utf-8)            cmd_res=subprocess.Popen(cmd,shell=True,                             stderr=subprocess.PIPE,                             stdout=subprocess.PIPE)            err=cmd_res.stderr.read()            if err:                res=err            else:                res=cmd_res.stdout.read()            conn.send(res)        except Exception:            break    conn.close()server.close()

client端:

#!/usr/bin/env python#-*-coding:utf-8-*-import socketclient=socket.socket(socket.AF_INET,socket.SOCK_STREAM)client.connect((127.0.0.1,8080))while True:    cmd=input(>>).strip()    if len(cmd) == 0:continue    client.send(bytes(cmd,encoding=utf-8))    client_date=client.recv(1024)    print(client_date.decode(gbk))client.close()

仔细看执行结果:

技术分享

看执行结果可知:当输入ipconfig的时候,执行的任然是dir命令的执行结果,则这就是发生粘包现象的结果,为什么发生这种现象呢?是因为client端接受执行结果的时候,接受的1024字节的数据,而执行结果的数据大于1024个字节,所以发生这种现象

第六、 什么是粘包

声明:第五中看到的是tcp发生粘包的现象,并且粘包的现象只会发生在tcp连接中,为什么呢?请看下面分解

发送端可以是一K一K地发送数据,而接收端的应用程序可以两K两K地提走数据,当然也有可能一次提走3K或6K数据,或者一次只提走几个字节的数据,也就是说,应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束

所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

  1. TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
  2. UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。

第七、 解决粘包的方法

方法一:

方法二:

第八、socketserver实现并发

python之socket编程