socket编程

一、客户端/服务器架构

  C/S架构server端要求:

    1.力求一直提供服务

    2.要绑定一个唯一的地址,让客户端能够明确得找到

  即C/S架构,包括

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

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

  美好的愿望:

  最常用的服务器是web服务器。一台机器里放一些网页或web应用程序,然后启动服务。这样的服务器的任务就是接受客户的请求,把网页发给客户(如用户计算机上的浏览器),然后等待下一个客户请求。这些服务启动后的目标就是“永远运行下去”。虽然它们不可能实现这样的目标,但只要没有关机或硬件出错等外力干扰,它们就能运行非常长的一段时间。

  生活中的C/S架构:

  老男孩是S端,所有学员是C端

  饭店是S端,多有食客是C端

  互联网中处处是C/S架构(黄色网站是服务端,你的浏览器是客户端;腾讯作为服务端为提供的视频,你得下个腾讯视频客户端才能看到狗日的视频)

  C/S架构与socket的关系:

  我们学习socket就是为了完成C/S架构的开发

二、osi七层

引子:

须知一个完整的计算机系统是由硬件、操作系统、应用软件三者组成,具备了这三个条件,一台计算机系统就可以自己跟自己玩了(大个单机游戏,玩个扫雷啥的)

如果你要跟别人一切玩,那你就需要上网了(访问个黄色网站,发个黄色微博啥的),互联网的核心就是由一堆协议组成,协议就是标准,全世界人通信的标准是英语,如果把计算机比做人,互联网协议就是计算机界的英语。所有的计算机都学会了互联网协议,那所有的计算机都可以按照统一的标准去收发信息从而完成通信了。人们按照分工不同把互联网协议从逻辑上划分了层级

  网络通信原理:http://www.cnblogs.com/linhaifeng/articles/5937962.html

为何学习socket一定要先学习互联网协议:

  1.首先:本节课程的目标就是教会你如何基于socket编程,来开发一款自己的C/S架构软件

  2.其次:C/S架构的软件(软件属于应用层)是基于网络进行通信的

  3.然后:网络的核心即一堆协议,协议即标准,你想开发一款基于网络通信的软件,就必须遵循这些标准

  4.最后:就让我们从这些标准开始研究,开启我们的socket编程之旅

物理层:发送电信号用的,需要底层物理量结机制建立连接,网卡,网线,路由器,但是单纯的电信号没有意义,于是有了数据链路层。

数据链路层:把电信号分组,根据以太网协议(一组电信号有一个数据包,叫做“帧”,每一数据帧分成:报头head和数据data两部分,头18个字节用来描述数据的)

有了mac地址就可以基于广播方式进行通信了,但是广播只能在一个子网里进行通信,要实现跨子网通信就只有mac地址就不行了,然后就需要网络层了

网络层:IP协议

比如访问百度服务器,我们通过IP地址实现跨子网后到达百度服务器的子网后又通过arp协议把ip地址解析成mac地址,最后定位到百度服务器的位置,所以通过ip地址可以找到唯一一台机器,但是是由arp协议进行辅助的,但是这个时候还没有完,我们要找到机器上的软件,这个时候就需要端口也就是传输层了,tcp和udp都是基于端口的协议,通过他我们到达了应用层也就是要找的软件,妈的累死了。

socket在传输层和应用层之间,就是封装了一堆TCP/UDP协议的简单接口,就是给我们这些美男子用的

TCP/IP协议族包括运输层、网络层、链路层。想在你知道TCP/IP与UDP的关系了吧。

三、socket层

四、socket是什么

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

  所以,我们无需深入理解tcp/udp协议,socket已经为我们封装好了,我们只需要遵循socket的规定去编程,写出程序自然就是遵循tcp/udp标准的。

1
2
3
4
#扫盲
也有人将socket说成ip+port,ip就是用来标识互联网中的一台主机的位置,而port是用来标识这台机器上的一个应用程序,ip地址是配置到网卡上的,而port是应用程序开启的,ip与port的绑定就标识了互联网中独一无二的一个应用程序
 
而程序的pid是同一台机器上的不同进程或者线程的标识

五、套接字发展史及分类

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

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

套接字家族的名字:AF_UNIX

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

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

套接字家族的名字:AF_INET

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

六、套接字流程

   一个生活中的场景,你要打电话给一个朋友,先拨号,朋友听到电话铃声后提起电话,这是你和你的朋友就建立起了连接,就可以讲话了。等交流结束,挂断电话结束此次交谈。生活中的场景就解释了这工作原理,也许TCP/IP协议族就是诞生于生活中,这也不一定。

  先从服务器端说起,服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connet),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束

socket()模块函数用法

1
2
3
4
5
6
7
8
9
10
11
12
import socket
socket.socket(socket_family,socket_type,protocal=0)
socket_family 可以是 AF_UNIX 或 AF_INET。socket_type 可以是 SOCK_STREAM 或 SOCK_DGRAM。protocol 一般不填,默认值为 0
 
获取tcp/ip套接字
tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
获取udp/ip套接字
udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
 
由于 socket 模块中有太多的属性。我们在这里破例使用了'from module import *'语句。使用 'from socket import *',我们就把 socket 模块里的所有属性都带到我们的命名空间里了,这样能 大幅减短我们的代码。
例如tcpSock = socket(AF_INET, SOCK_STREAM)

  服务器套接字函数

  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() 创建一个与该套接字相关的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
1:用打电话的流程快速描述socket通信
2:服务端和客户端加上基于一次链接的循环通信
3:客户端发送空,卡主,证明是从哪个位置卡的
服务端:
from socket import *
phone=socket(AF_INET,SOCK_STREAM)
phone.bind(('127.0.0.1',8081))
phone.listen(5)
 
conn,addr=phone.accept()
while True:
    data=conn.recv(1024)
    print('server===>')
    print(data)
    conn.send(data.upper())
conn.close()
phone.close()
客户端:
from socket import *
 
phone=socket(AF_INET,SOCK_STREAM)
phone.connect(('127.0.0.1',8081))
 
while True:
    msg=input('>>: ').strip()
    phone.send(msg.encode('utf-8'))
    print('client====>')
    data=phone.recv(1024)
    print(data)
 
说明卡的原因:缓冲区为空recv就卡住,引出原理图
 
 
 
4.演示客户端断开链接,服务端的情况,提供解决方法
 
5.演示服务端不能重复接受链接,而服务器都是正常运行不断来接受客户链接的
 
6:简单演示udp
服务端
from socket import *
phone=socket(AF_INET,SOCK_DGRAM)
phone.bind(('127.0.0.1',8082))
while True:
    msg,addr=phone.recvfrom(1024)
    phone.sendto(msg.upper(),addr)
客户端
from socket import *
phone=socket(AF_INET,SOCK_DGRAM)
while True:
    msg=input('>>: ')
    phone.sendto(msg.encode('utf-8'),('127.0.0.1',8082))
    msg,addr=phone.recvfrom(1024)
    print(msg)
 
udp客户端可以并发演示
udp客户端可以输入为空演示,说出recvfrom与recv的区别,暂且不提tcp流和udp报的概念,留到粘包去说

七、基于TCP的套接字

tcp服务端

conn就是客户端套接字对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#服务端:
# from socket import *
# phone=socket(AF_INET,SOCK_STREAM) #拿到tcp/ip套接字
# phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #加入一条socket配置,重用ip和端口
# phone.bind(("127.0.0.1",8080)) #参数是ip+端口号的元组
# phone.listen(5) #服务端最多挂起链接数
# while True: #链接循环
#     conn,addr=phone.accept() #conn是链接对象就是客户端套接字对象,addr是客户端的ip+端口号
#     while True: #通信循环,就是recv和send
#         try:
#             data=conn.recv() #接收需要有个参数buffersize
#             print(data.decode("utf8"))
#             conn.send(data.upper()) #发包,这个时候是操作系统发的,所以编码格式也是操作系统默认的
#         except Exception:
#             break
#     conn.close()
# phone.close()

tcp客户端

1
2
3
4
5
6
7
8
9
10
11
#客户端
# from socket import *
# phone=socket(AF_INET,SOCK_STREAM)#拿到一个tcpsocket对象
# phone.connect(("127.0.0.1",8080)) #建立连接
# while True: #通信循环
#     msg=input(">>:")
#     if not msg:continue
#     phone.send(msg.encode("utf8"))
#     res=phone.recv(1024) #接收需要一个参数buffersize
#     print(res.decode("gbk")) #如果是Windows系统就用gbk,如果是Linux就用utf-8
# phone.close()

  问题:

  有的同学在重启服务端时可能会遇到

  这个是由于你的服务器仍然存在四次挥手的time_wait状态在占用地址(如果不懂,请深入研究1.tcp三次握手,四次挥手;2.syn洪水攻击;3.服务器高并发情况下会有大量的time_wait状态的优化方法)

  解决方法:

1
2
3
4
5
#方法一<br>#加入一条socket配置,重用ip和端口
 
phone=socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #就是它,在bind前加
phone.bind(('127.0.0.1',8080))

  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#方法二
发现系统存在大量TIME_WAIT状态的连接,通过调整linux内核参数解决,
vi /etc/sysctl.conf
 
编辑文件,加入以下内容:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30
  
然后执行 /sbin/sysctl -p 让参数生效。
  
net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;
 
net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
 
net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
 
net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间

 八、基于UDP的套接字

udp服务端

1
2
3
4
5
6
7
8
9
10
11
12
import socket
us=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)#产生一个udp对象
us.bind(("127.0.0.1",8080)) #绑定ip+端口号,
while True: #udp接收和发送都是两个值,先信息,后地址
    try:
        res,addr=us.recvfrom(1024) #接收信息和客户端地址
        print(res.decode("utf8"))
        msg=input(">>:").strip()
        us.sendto(msg.encode("utf-8"),addr) #发送信息和客户端地址
    except Exception:
        break
us.close()

udp客户端

1
2
3
4
5
6
7
8
import socket
us=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
while True:
    msg=input(">>:").strip()
    us.sendto(msg.encode("utf-8"),("127.0.0.1",8080))
    res,addr=us.recvfrom(1024)
    print(res.decode("utf-8"))
us.close()

qq聊天(基于udp连接,所以可以同时多个客户端去跟服务端通信)

1
2
3
4
5
6
7
8
9
10
11
#qq聊天服务端
# import socket
# ip_port=("127.0.0.1",8080)
# udp_server_sock=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
# udp_server_sock.bind(ip_port)
# while True:
#     qq_msg,addr=udp_server_sock.recvfrom(1024)
#     print("来自[%s:%s]的一条消息:\033[1;44m%s\033[0m"%(addr[0],addr[1],qq_msg.decode("utf-8")))
#     back_msg=input("回复消息:").strip()
#     udp_server_sock.sendto(back_msg.encode("utf-8"),addr)
# udp_server_sock.close()

qq客户端1

1
2
3
4
5
6
7
8
9
#qq聊天客户端1
# import socket
# us=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
# while True:
#     msg=input(">>:").strip()
#     us.sendto(msg.encode("utf-8"),("127.0.0.1",8080))
#     res,addr=us.recvfrom(1024)
#     print(res.decode("utf-8"))
# us.close()

qq客户端2

1
2
3
4
5
6
7
8
9
#qq聊天客户端2
# import socket
# us=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
# while True:
#     msg=input(">>:").strip()
#     us.sendto(msg.encode("utf-8"),("127.0.0.1",8080))
#     res,addr=us.recvfrom(1024)
#     print(res.decode("utf-8"))
# us.close()

九、recv与recvfrom的区别

  part1:收发消息的原理需知晓:发消息都是将数据发送到己端的发送缓冲中 ,收消息都是从己端的缓冲区中收

   1.tcp:send发消息,recv收消息

   2.udp:sendto发消息,recvfrom收消息

  part2:send与sendto

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

   1.send(bytes_data):发送数据流,数据流bytes_data若为空,自己这段的缓冲区也为空,操作系统不会控制tcp协议发空包

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

  part3:recv与recvfrom

   1.tcp协议:

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

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

空,也跟没有一个样。

    (3)tcp基于链接通信

      基于链接,则需要listen(backing),指定半连接池的大小

      基于链接,必须先运行服务端,然后客户端发起链接请求

      对于mac/Linux系统:如果一端断开了链接,那另一端的链接也跟着完蛋recv不会阻塞,收到的是空(解决方法是:服务端在接收消息后加上if判断,空消息就break掉通信循环)

      对于Windows系统:如果一端断开了链接,那另一端的链接也跟着完蛋recv将不会阻塞,收到的是空(解决方法是:服务端通信循环内加异常处理,捕捉到异常后就break掉通讯循环)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
import subprocess
from socket import *
 
phone=socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
phone.bind(('127.0.0.1',8080))
phone.listen(5)
 
conn,addr=phone.accept()
 
while True:
    data=conn.recv(1024)
    print('from client msg is ',data)
    conn.send(data.upper())
 
服务端

  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
import subprocess
from socket import *
 
phone=socket(AF_INET,SOCK_STREAM)
phone.connect(('127.0.0.1',8080))
 
 
while True:
    msg=input('>>: ')
    phone.send(msg.encode('utf-8'))
    print('Client message has been sent')
 
    data=phone.recv(1024)
    print('from server msg is ',data.decode('utf-8'))
phone.close()
 
客户端

   2.udp协议

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

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

    (3)udp无链接

      无链接,因而无需listen(backing),更加没有什么连接池之说了

      无链接,udp的sendto不用管是否有一个正在运行的服务端,可以己端一个劲的发消息,只不过数据丢失

      recvfrom收的数据小于sendinto发送的数据时,在mac和Linux系统数据直接丢失,在Windows系统上发送的比收的大直接报错

      只有sendto发送数据没有recvfrom收数据,数据丢失

客户端发送空,看服务端结果--->验证(1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
from socket import *
 
ip_port=('127.0.0.1',9003)
bufsize=1024
 
udp_server=socket(AF_INET,SOCK_DGRAM)
udp_server.bind(ip_port)
 
while True:
    data1,addr=udp_server.recvfrom(bufsize)
    print(data1)
 
服务端
1
2
3
4
5
6
7
8
9
10
11
from socket import *
ip_port=('127.0.0.1',9003)
bufsize=1024
 
udp_client=socket(AF_INET,SOCK_DGRAM)
 
while True:
    msg=input('>>: ')
    udp_client.sendto(msg.encode('utf-8'),ip_port) #发送空,发现服务端可以接收空
 
客户端

分别运行服务端,客户端--->验证(2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
from socket import *
 
ip_port=('127.0.0.1',9003)
bufsize=1024
 
udp_server=socket(AF_INET,SOCK_DGRAM)
udp_server.bind(ip_port)
 
data1,addr=udp_server.recvfrom(1)
print('第一次收了 ',data1)
data2,addr=udp_server.recvfrom(1)
print('第二次收了 ',data2)
data3,addr=udp_server.recvfrom(1)
print('第三次收了 ',data3)
print('--------结束----------')
 
服务端 
1
2
3
4
5
6
7
8
9
10
11
from socket import *
ip_port=('127.0.0.1',9003)
bufsize=1024
 
udp_client=socket(AF_INET,SOCK_DGRAM)
 
udp_client.sendto(b'hello',ip_port)
udp_client.sendto(b'world',ip_port)
udp_client.sendto(b'egon',ip_port)
 
客户端 

不运行服务端,单独运行客户端,一点问题没有,但是消息丢了--->验证(3)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
from socket import *
 
ip_port=('127.0.0.1',9003)
bufsize=1024
 
udp_server=socket(AF_INET,SOCK_DGRAM)
udp_server.bind(ip_port)
 
data1,addr=udp_server.recvfrom(bufsize)
print('第一次收了 ',data1)
data2,addr=udp_server.recvfrom(bufsize)
print('第二次收了 ',data2)
data3,addr=udp_server.recvfrom(bufsize)
print('第三次收了 ',data3)
print('--------结束----------')
 
服务端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from socket import *
import time
ip_port=('127.0.0.1',9003)
bufsize=1024
 
udp_client=socket(AF_INET,SOCK_DGRAM)
 
udp_client.sendto(b'hello',ip_port)
udp_client.sendto(b'world',ip_port)
udp_client.sendto(b'egon',ip_port)
 
print('客户端发完消息啦')
time.sleep(100)
 
客户端

注意:

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

2.上面的udp程序,你注释任何一条客户端的sendto,服务端都会卡住,为什么?因为服务端有几个recvfrom就要对应几个sendto,哪怕是sendto(b"")那也要有。

十、粘包现象

让我们基于tcp先制作一个远程执行命令的程序(1:执行错误命令;2:执行ls;3.执行ifconfig)

注意注意注意:

res=subprocess.Popen(cmd.decode("utf-8"),

            shell = True,

            stderr=subprocess.PIPE,

            stdout=subprocess.PIPE) 

的结果的编码是以当前所在的系统为准的,如果是Windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码

且只能从管道里读一次结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#-*-coding:utf-8-*-
__author__ = "Wuyongqing"
from socket import *
import subprocess
ip_port=("127.0.0.1",8080)
bufsize=1024
 
tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)
 
while True:
    conn,addr=tcp_socket_server.accept()
    print("客户端",addr)
    while True:
        cmd=conn.recv(bufsize)
        if len(cmd) == 0:break
        act_res=subprocess.Popen(cmd.decode("utf-8"),
                                                     shell=True,
                                                     stdout=subprocess.PIPE,
                                                     stderr=subprocess.PIPE)
        act_err=act_res.stderr.read()
        if act_err:
            ret=act_err
        else:
            ret=act_res.stdout.read()
        conn.sendall(ret)
1
2
3
4
5
6
7
8
9
10
11
12
13
__author__="Wuyongqiang"
import socket
bufsize=1024
ip_port=("127.0.0.1",8080)
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect(ip_port)
while True:
    msg=input(">>:").strip()
    if len(msg) == 0:continue
    if msg == "quit":break
    s.send(msg.encode("utf-8"))
    act_res=s.recv(bufsize)
    print(act_res.decode("utf-8"),end="")

  上述程序是基于tcp的socket,在运行时会发生粘包

  让我们再基于udp制作一个远程执行命令的程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#-*-coding:utf-8-*-
__author__="Wuyongqiang"
from socket import *
import subprocess
ip_port=("127.0.0.1",8080)
bufsize=1024
udp_server=socket(AF_INET,SOCK_DGRAM)
udp_server.bind(ip_port)
while True:
    #收消息
    cmd,addr=udp_server.recvfrom(bufsize)
    print("用户命令--->",cmd)
    #逻辑处理
    res=subprocess.Popen(cmd.decode("utf-8"),
                         shell=True,
                         stderr=subprocess.PIPE,
                         stdout=subprocess.PIPE,
                         )
    err=res.stderr.read()
    print("错误===>",err)
    if err:
        back_msg=err
    else:
        back_msg=res.stdout.read()
    print("返回结果",back_msg)
    #发消息
    udp_server.sendto(back_msg,addr)
udp_server.close()

上述程序是基于udp的socket,在运行时永远不会发生粘包

十一、什么是粘包

须知:只有TCP有粘包现象,UDP永远不会粘包,为何~~

首先要掌握一个socket收发消息的原理

发送端可以是—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包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说就容易进行区分处理了。即面向消息的通信是有消息保护边界的。

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

udp的recvfrom是阻塞的,一个recvfrom(x)必须对一个sendto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠

tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接受,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

 两种情况下会发生粘包。

发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据很小,会合在一起,产生粘包)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#_*_coding:utf-8_*_
from socket import *
ip_port=('127.0.0.1',8080)
 
tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)
 
 
conn,addr=tcp_socket_server.accept()
 
 
data1=conn.recv(10)
data2=conn.recv(10)
 
print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))
 
conn.close()
 
服务端
1
2
3
4
5
6
7
8
9
10
11
12
13
#_*_coding:utf-8_*_
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080)
 
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port)
 
 
s.send('hello'.encode('utf-8'))
s.send('feng'.encode('utf-8'))
 
客户端

接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包) 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#_*_coding:utf-8_*_
from socket import *
ip_port=('127.0.0.1',8080)
 
tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)
 
 
conn,addr=tcp_socket_server.accept()
 
 
data1=conn.recv(2) #一次没有收完整
data2=conn.recv(10)#下次收的时候,会先取旧的数据,然后取新的
 
print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))
 
conn.close()
 
服务端
1
2
3
4
5
6
7
8
9
10
11
12
#_*_coding:utf-8_*_
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080)
 
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port)
 
 
s.send('hello feng'.encode('utf-8'))
 
客户端

拆包的发生情况

当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。

补充问题一:为何tcp是可靠传输,udp是不可靠传输

基于tcp的数据传输请参考另一篇文章http://www.cnblogs.com/linhaifeng/articles/5937962.html,tcp在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以tcp是可靠的

而udp发送数据,对端是不会返回确认信息的,因此不可靠

补充问题二:send(字节流)和recv(1024)及sendall

recv里指定的1024意思是从缓存里一次拿出1024个字节的数据

send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失

十二、解决粘包的low逼处理方法

问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据

low版本的解决方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#_*_coding:utf-8_*_
import socket,subprocess
ip_port=('127.0.0.1',8080)
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 
s.bind(ip_port)
s.listen(5)
 
while True:
    conn,addr=s.accept()
    print('客户端',addr)
    while True:
        msg=conn.recv(1024)
        if not msg:break
        res=subprocess.Popen(msg.decode('utf-8'),shell=True,\
                            stdin=subprocess.PIPE,\
                         stderr=subprocess.PIPE,\
                         stdout=subprocess.PIPE)
        err=res.stderr.read()
        if err:
            ret=err
        else:
            ret=res.stdout.read()
        data_length=len(ret)
        conn.send(str(data_length).encode('utf-8'))
        data=conn.recv(1024).decode('utf-8')
        if data == 'recv_ready':
            conn.sendall(ret)
    conn.close()
 
服务端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#_*_coding:utf-8_*_
import socket,time
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(('127.0.0.1',8080))
 
while True:
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    if msg == 'quit':break
 
    s.send(msg.encode('utf-8'))
    length=int(s.recv(1024).decode('utf-8'))
    s.send('recv_ready'.encode('utf-8'))
    send_size=0
    recv_size=0
    data=b''
    while recv_size < length:
        data+=s.recv(1024)
        recv_size+=len(data)
 
 
    print(data.decode('utf-8'))
 
客户端

为何low:

程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网路延迟带来的性能损耗。

十三、高逼格解决粘包的方法

为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据

struct模块

该模块可以把一个类型,如数字,转成固定长度的bytes

struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #这个是范围

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import json,struct
#假设通过客户端上传1T:1073741824000的文件a.txt
 
#为避免粘包,必须自定制报头
header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T数据,文件路径和md5值
 
#为了该报头能传送,需要序列化并且转为bytes
head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并转成bytes,用于传输
 
#为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节
head_len_bytes=struct.pack('i',len(head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度
 
#客户端开始发送
conn.send(head_len_bytes) #先发报头的长度,4个bytes
conn.send(head_bytes) #再发报头的字节格式
conn.sendall(文件内容) #然后发真实内容的字节格式
 
#服务端开始接收
head_len_bytes=s.recv(4) #先收报头4个bytes,得到报头长度的字节格式
x=struct.unpack('i',head_len_bytes)[0] #提取报头的长度
 
head_bytes=s.recv(x) #按照报头长度x,收取报头的bytes格式
header=json.loads(json.dumps(header)) #提取报头
 
#最后根据报头的内容提取真实的数据,比如
real_data_len=s.recv(header['file_size'])
s.recv(real_data_len)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#_*_coding:utf-8_*_
#http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html
import struct
import binascii
import ctypes
 
values1 = (1, 'abc'.encode('utf-8'), 2.7)
values2 = ('defg'.encode('utf-8'),101)
s1 = struct.Struct('I3sf')
s2 = struct.Struct('4sI')
 
print(s1.size,s2.size)
prebuffer=ctypes.create_string_buffer(s1.size+s2.size)
print('Before : ',binascii.hexlify(prebuffer))
# t=binascii.hexlify('asdfaf'.encode('utf-8'))
# print(t)
 
 
s1.pack_into(prebuffer,0,*values1)
s2.pack_into(prebuffer,s1.size,*values2)
 
print('After pack',binascii.hexlify(prebuffer))
print(s1.unpack_from(prebuffer,0))
print(s2.unpack_from(prebuffer,s1.size))
 
s3=struct.Struct('ii')
s3.pack_into(prebuffer,0,123,123)
print('After pack',binascii.hexlify(prebuffer))
print(s3.unpack_from(prebuffer,0))
 
关于struct的详细用法

  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import socket,struct,json
import subprocess
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加
 
phone.bind(('127.0.0.1',8080))
 
phone.listen(5)
 
while True:
    conn,addr=phone.accept()
    while True:
        cmd=conn.recv(1024)
        if not cmd:break
        print('cmd: %s' %cmd)
 
        res=subprocess.Popen(cmd.decode('utf-8'),
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
        err=res.stderr.read()
        print(err)
        if err:
            back_msg=err
        else:
            back_msg=res.stdout.read()
 
 
        conn.send(struct.pack('i',len(back_msg))) #先发back_msg的长度
        conn.sendall(back_msg) #在发真实的内容
 
    conn.close()
 
服务端(自定制报头)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#_*_coding:utf-8_*_
import socket,time,struct
 
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(('127.0.0.1',8080))
 
while True:
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    if msg == 'quit':break
 
    s.send(msg.encode('utf-8'))
 
 
 
    l=s.recv(4)
    x=struct.unpack('i',l)[0]
    print(type(x),x)
    # print(struct.unpack('I',l))
    r_s=0
    data=b''
    while r_s < x:
        r_d=s.recv(1024)
        data+=r_d
        r_s+=len(r_d)
 
    # print(data.decode('utf-8'))
    print(data.decode('gbk')) #windows默认gbk编码
 
客户端(自定制报头)

我们可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节(4个自己足够用了)

 

发送时:

先发报头长度

再编码报头内容然后发送

最后发真实内容

接收时:

先手报头长度,用struct取出来

根据取出的长度收取报头内容,然后解码,反序列化

从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import socket,struct,json
import subprocess
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加
 
phone.bind(('127.0.0.1',8080))
 
phone.listen(5)
 
while True:
    conn,addr=phone.accept()
    while True:
        cmd=conn.recv(1024)
        if not cmd:break
        print('cmd: %s' %cmd)
 
        res=subprocess.Popen(cmd.decode('utf-8'),
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
        err=res.stderr.read()
        print(err)
        if err:
            back_msg=err
        else:
            back_msg=res.stdout.read()
 
        headers={'data_size':len(back_msg)}
        head_json=json.dumps(headers)
        head_json_bytes=bytes(head_json,encoding='utf-8')
 
        conn.send(struct.pack('i',len(head_json_bytes))) #先发报头的长度
        conn.send(head_json_bytes) #再发报头
        conn.sendall(back_msg) #在发真实的内容
 
    conn.close()
 
 服务端:定制稍微复杂一点的报头
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from socket import *
import struct,json
 
ip_port=('127.0.0.1',8080)
client=socket(AF_INET,SOCK_STREAM)
client.connect(ip_port)
 
while True:
    cmd=input('>>: ')
    if not cmd:continue
    client.send(bytes(cmd,encoding='utf-8'))
 
    head=client.recv(4)
    head_json_len=struct.unpack('i',head)[0]
    head_json=json.loads(client.recv(head_json_len).decode('utf-8'))
    data_len=head_json['data_size']
 
    recv_size=0
    recv_data=b''
    while recv_size < data_len:
        recv_data+=client.recv(1024)
        recv_size+=len(recv_data)
 
    print(recv_data.decode('utf-8'))
    #print(recv_data.decode('gbk')) #windows默认gbk编码
 
客户端

FTP作业:上传下载文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import socket
import struct
import json
import subprocess
import os
 
class MYTCPServer:
    address_family = socket.AF_INET
 
    socket_type = socket.SOCK_STREAM
 
    allow_reuse_address = False
 
    max_packet_size = 8192
 
    coding='utf-8'
 
    request_queue_size = 5
 
    server_dir='file_upload'
 
    def __init__(self, server_address, bind_and_activate=True):
        """Constructor.  May be extended, do not override."""
        self.server_address=server_address
        self.socket = socket.socket(self.address_family,
                                    self.socket_type)
        if bind_and_activate:
            try:
                self.server_bind()
                self.server_activate()
            except:
                self.server_close()
                raise
 
    def server_bind(self):
        """Called by constructor to bind the socket.
        """
        if self.allow_reuse_address:
            self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.socket.bind(self.server_address)
        self.server_address = self.socket.getsockname()
 
    def server_activate(self):
        """Called by constructor to activate the server.
        """
        self.socket.listen(self.request_queue_size)
 
    def server_close(self):
        """Called to clean-up the server.
        """
        self.socket.close()
 
    def get_request(self):
        """Get the request and client address from the socket.
        """
        return self.socket.accept()
 
    def close_request(self, request):
        """Called to clean up an individual request."""
        request.close()
 
    def run(self):
        while True:
            self.conn,self.client_addr=self.get_request()
            print('from client ',self.client_addr)
            while True:
                try:
                    head_struct = self.conn.recv(4)
                    if not head_struct:break
 
                    head_len = struct.unpack('i', head_struct)[0]
                    head_json = self.conn.recv(head_len).decode(self.coding)
                    head_dic = json.loads(head_json)
 
                    print(head_dic)
                    #head_dic={'cmd':'put','filename':'a.txt','filesize':123123}
                    cmd=head_dic['cmd']
                    if hasattr(self,cmd):
                        func=getattr(self,cmd)
                        func(head_dic)
                except Exception:
                    break
 
    def put(self,args):
        file_path=os.path.normpath(os.path.join(
            self.server_dir,
            args['filename']
        ))
 
        filesize=args['filesize']
        recv_size=0
        print('----->',file_path)
        with open(file_path,'wb') as f:
            while recv_size < filesize:
                recv_data=self.conn.recv(self.max_packet_size)
                f.write(recv_data)
                recv_size+=len(recv_data)
                print('recvsize:%s filesize:%s' %(recv_size,filesize))
 
 
tcpserver1=MYTCPServer(('127.0.0.1',8080))
 
tcpserver1.run()
 
 
 
 
 
 
#下列代码与本题无关
class MYUDPServer:
 
    """UDP server class."""
    address_family = socket.AF_INET
 
    socket_type = socket.SOCK_DGRAM
 
    allow_reuse_address = False
 
    max_packet_size = 8192
 
    coding='utf-8'
 
    def get_request(self):
        data, client_addr = self.socket.recvfrom(self.max_packet_size)
        return (data, self.socket), client_addr
 
    def server_activate(self):
        # No need to call listen() for UDP.
        pass
 
    def shutdown_request(self, request):
        # No need to shutdown anything.
        self.close_request(request)
 
    def close_request(self, request):
        # No need to close anything.
        pass
 
服务端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import socket
import struct
import json
import os
 
 
 
class MYTCPClient:
    address_family = socket.AF_INET
 
    socket_type = socket.SOCK_STREAM
 
    allow_reuse_address = False
 
    max_packet_size = 8192
 
    coding='utf-8'
 
    request_queue_size = 5
 
    def __init__(self, server_address, connect=True):
        self.server_address=server_address
        self.socket = socket.socket(self.address_family,
                                    self.socket_type)
        if connect:
            try:
                self.client_connect()
            except:
                self.client_close()
                raise
 
    def client_connect(self):
        self.socket.connect(self.server_address)
 
    def client_close(self):
        self.socket.close()
 
    def run(self):
        while True:
            inp=input(">>: ").strip()
            if not inp:continue
            l=inp.split()
            cmd=l[0]
            if hasattr(self,cmd):
                func=getattr(self,cmd)
                func(l)
 
 
    def put(self,args):
        cmd=args[0]
        filename=args[1]
        if not os.path.isfile(filename):
            print('file:%s is not exists' %filename)
            return
        else:
            filesize=os.path.getsize(filename)
 
        head_dic={'cmd':cmd,'filename':os.path.basename(filename),'filesize':filesize}
        print(head_dic)
        head_json=json.dumps(head_dic)
        head_json_bytes=bytes(head_json,encoding=self.coding)
 
        head_struct=struct.pack('i',len(head_json_bytes))
        self.socket.send(head_struct)
        self.socket.send(head_json_bytes)
        send_size=0
        with open(filename,'rb') as f:
            for line in f:
                self.socket.send(line)
                send_size+=len(line)
                print(send_size)
            else:
                print('upload successful')
 
 
 
 
client=MYTCPClient(('127.0.0.1',8080))
 
client.run()
 
客户端

十四、认证客户端链接的合法性

如果你想在分布式系统中实现一个简单的客户端链接认证功能,又不像SSL那么复杂,那么利用hmac+加盐的方式来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
from socket import *
import hmac,os
 
secret_key=b'linhaifeng bang bang bang'
def conn_auth(conn):
    '''
    认证客户端链接
    :param conn:
    :return:
    '''
    print('开始验证新链接的合法性')
    msg=os.urandom(32)
    conn.sendall(msg)
    h=hmac.new(secret_key,msg)
    digest=h.digest()
    respone=conn.recv(len(digest))
    return hmac.compare_digest(respone,digest)
 
def data_handler(conn,bufsize=1024):
    if not conn_auth(conn):
        print('该链接不合法,关闭')
        conn.close()
        return
    print('链接合法,开始通信')
    while True:
        data=conn.recv(bufsize)
        if not data:break
        conn.sendall(data.upper())
 
def server_handler(ip_port,bufsize,backlog=5):
    '''
    只处理链接
    :param ip_port:
    :return:
    '''
    tcp_socket_server=socket(AF_INET,SOCK_STREAM)
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen(backlog)
    while True:
        conn,addr=tcp_socket_server.accept()
        print('新连接[%s:%s]' %(addr[0],addr[1]))
        data_handler(conn,bufsize)
 
if __name__ == '__main__':
    ip_port=('127.0.0.1',9999)
    bufsize=1024
    server_handler(ip_port,bufsize)
 
服务端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
from socket import *
import hmac,os
 
secret_key=b'linhaifeng bang bang bang'
def conn_auth(conn):
    '''
    验证客户端到服务器的链接
    :param conn:
    :return:
    '''
    msg=conn.recv(32)
    h=hmac.new(secret_key,msg)
    digest=h.digest()
    conn.sendall(digest)
 
def client_handler(ip_port,bufsize=1024):
    tcp_socket_client=socket(AF_INET,SOCK_STREAM)
    tcp_socket_client.connect(ip_port)
 
    conn_auth(tcp_socket_client)
 
    while True:
        data=input('>>: ').strip()
        if not data:continue
        if data == 'quit':break
 
        tcp_socket_client.sendall(data.encode('utf-8'))
        respone=tcp_socket_client.recv(bufsize)
        print(respone.decode('utf-8'))
    tcp_socket_client.close()
 
if __name__ == '__main__':
    ip_port=('127.0.0.1',9999)
    bufsize=1024
    client_handler(ip_port,bufsize)
 
客户端(合法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
from socket import *
 
def client_handler(ip_port,bufsize=1024):
    tcp_socket_client=socket(AF_INET,SOCK_STREAM)
    tcp_socket_client.connect(ip_port)
 
    while True:
        data=input('>>: ').strip()
        if not data:continue
        if data == 'quit':break
 
        tcp_socket_client.sendall(data.encode('utf-8'))
        respone=tcp_socket_client.recv(bufsize)
        print(respone.decode('utf-8'))
    tcp_socket_client.close()
 
if __name__ == '__main__':
    ip_port=('127.0.0.1',9999)
    bufsize=1024
    client_handler(ip_port,bufsize)
 
客户端(非法:不知道加密方式)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
from socket import *
import hmac,os
 
secret_key=b'linhaifeng bang bang bang1111'
def conn_auth(conn):
    '''
    验证客户端到服务器的链接
    :param conn:
    :return:
    '''
    msg=conn.recv(32)
    h=hmac.new(secret_key,msg)
    digest=h.digest()
    conn.sendall(digest)
 
def client_handler(ip_port,bufsize=1024):
    tcp_socket_client=socket(AF_INET,SOCK_STREAM)
    tcp_socket_client.connect(ip_port)
 
    conn_auth(tcp_socket_client)
 
    while True:
        data=input('>>: ').strip()
        if not data:continue
        if data == 'quit':break
 
        tcp_socket_client.sendall(data.encode('utf-8'))
        respone=tcp_socket_client.recv(bufsize)
        print(respone.decode('utf-8'))
    tcp_socket_client.close()
 
if __name__ == '__main__':
    ip_port=('127.0.0.1',9999)
    bufsize=1024
    client_handler(ip_port,bufsize)
 
客户端(非法:不知道secret_key)

十五、socketserver实现并发

基于tcp的套接字,关键就是两个循环,一个链接循环,一个通信循环

socketserver模块中分两大类:server类(解决链接问题)和request类(解决通信问题)

server类:

request类:

继承关系:

以下述代码为例,分析socketserver源码:

ftpserver=socketserver.ThreadingTCPServer(('127.0.0.1',8080),FtpServer)
ftpserver.serve_forever()

查找属性的顺序:ThreadingTCPServer->ThreadingMixIn->TCPServer->BaseServer

  1. 实例化得到ftpserver,先找类ThreadingTCPServer的__init__,在TCPServer中找到,进而执行server_bind,server_active
  2. 找ftpserver下的serve_forever,在BaseServer中找到,进而执行self._handle_request_noblock(),该方法同样是在BaseServer中
  3. 执行self._handle_request_noblock()进而执行request, client_address = self.get_request()(就是TCPServer中的self.socket.accept()),然后执行self.process_request(request, client_address)
  4. 在ThreadingMixIn中找到process_request,开启多线程应对并发,进而执行process_request_thread,执行self.finish_request(request, client_address)
  5. 上述四部分完成了链接循环,本部分开始进入处理通讯部分,在BaseServer中找到finish_request,触发我们自己定义的类的实例化,去找__init__方法,而我们自己定义的类没有该方法,则去它的父类也就是BaseRequestHandler中找....

源码分析总结:

基于tcp的socketserver我们自己定义的类中的

  1. self.server即套接字对象
  2. self.request即一个链接
  3. self.client_address即客户端地址

基于udp的socketserver我们自己定义的类中的

  1. self.request是一个元组(第一个元素是客户端发来的数据,第二部分是服务端的udp套接字对象),如(b'adsf', <socket.socket fd=200, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=0, laddr=('127.0.0.1', 8080)>)
  2. self.client_address即客户端地址

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import socketserver
import struct
import json
import os
class FtpServer(socketserver.BaseRequestHandler):
    coding='utf-8'
    server_dir='file_upload'
    max_packet_size=1024
    BASE_DIR=os.path.dirname(os.path.abspath(__file__))
    def handle(self):
        print(self.request)
        while True:
            data=self.request.recv(4)
            data_len=struct.unpack('i',data)[0]
            head_json=self.request.recv(data_len).decode(self.coding)
            head_dic=json.loads(head_json)
            # print(head_dic)
            cmd=head_dic['cmd']
            if hasattr(self,cmd):
                func=getattr(self,cmd)
                func(head_dic)
    def put(self,args):
        file_path = os.path.normpath(os.path.join(
            self.BASE_DIR,
            self.server_dir,
            args['filename']
        ))
 
        filesize = args['filesize']
        recv_size = 0
        print('----->', file_path)
        with open(file_path, 'wb') as f:
            while recv_size < filesize:
                recv_data = self.request.recv(self.max_packet_size)
                f.write(recv_data)
                recv_size += len(recv_data)
                print('recvsize:%s filesize:%s' % (recv_size, filesize))
 
 
ftpserver=socketserver.ThreadingTCPServer(('127.0.0.1',8080),FtpServer)
ftpserver.serve_forever()
 
FtpServer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import socket
import struct
import json
import os
 
 
 
class MYTCPClient:
    address_family = socket.AF_INET
 
    socket_type = socket.SOCK_STREAM
 
    allow_reuse_address = False
 
    max_packet_size = 8192
 
    coding='utf-8'
 
    request_queue_size = 5
 
    def __init__(self, server_address, connect=True):
        self.server_address=server_address
        self.socket = socket.socket(self.address_family,
                                    self.socket_type)
        if connect:
            try:
                self.client_connect()
            except:
                self.client_close()
                raise
 
    def client_connect(self):
        self.socket.connect(self.server_address)
 
    def client_close(self):
        self.socket.close()
 
    def run(self):
        while True:
            inp=input(">>: ").strip()
            if not inp:continue
            l=inp.split()
            cmd=l[0]
            if hasattr(self,cmd):
                func=getattr(self,cmd)
                func(l)
 
 
    def put(self,args):
        cmd=args[0]
        filename=args[1]
        if not os.path.isfile(filename):
            print('file:%s is not exists' %filename)
            return
        else:
            filesize=os.path.getsize(filename)
 
        head_dic={'cmd':cmd,'filename':os.path.basename(filename),'filesize':filesize}
        print(head_dic)
        head_json=json.dumps(head_dic)
        head_json_bytes=bytes(head_json,encoding=self.coding)
 
        head_struct=struct.pack('i',len(head_json_bytes))
        self.socket.send(head_struct)
        self.socket.send(head_json_bytes)
        send_size=0
        with open(filename,'rb') as f:
            for line in f:
                self.socket.send(line)
                send_size+=len(line)
                print(send_size)
            else:
                print('upload successful')
 
 
 
 
client=MYTCPClient(('127.0.0.1',8080))
 
client.run()
 
FtpClient

十六、线程池实现服务端

    1. 用内置的 ThreadPoolExecutor 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import threading
from concurrent.futures import ThreadPoolExecutor
from socket import AF_INET, SOCK_STREAM, socket
 
_local = threading.local()  # 只是想做测试用,不放也可以
 
 
def echo_client(sock, client_addr):
    """
    Handle a client connection
    """
    _local.conn = sock
    print(_local.conn)
    print('Got connection from', client_addr)
    while True:
        msg = sock.recv(65536)
        if not msg:
            break
        sock.sendall(msg)
    print('Client closed connection')
 
 
def echo_server(addr):
    pool = ThreadPoolExecutor(128)
    sock = socket(AF_INET, SOCK_STREAM)
    sock.bind(addr)
    sock.listen(5)
    while True:
        client_sock, client_addr = sock.accept()
        pool.submit(echo_client, client_sock, client_addr)
 
 
echo_server(('', 15000))

    2. 自定义线程池实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from queue import Queue
from socket import socket, AF_INET, SOCK_STREAM
from threading import Thread
 
 
def echo_client(q):
    """
    Handle a client connection
    """
    sock, client_addr = q.get()
    print('Got connection from', client_addr)
    try:
        while True:
            msg = sock.recv(65536)
            if not msg:
                break
            sock.sendall(msg)
    except ConnectionResetError as e:  # 捕获客户端强制关闭现有链接的错误
        print(e)
    print('Client closed connection')
    sock.close()
 
 
def echo_server(addr, nworkers):
    # Launch the client workers
    q = Queue()
    for n in range(nworkers):
        t = Thread(target=echo_client, args=(q,))
        t.daemon = True
        t.start()
 
    # Run the server
    sock = socket(AF_INET, SOCK_STREAM)
    sock.bind(addr)
    sock.listen(5)
    while True:
        client_sock, client_addr = sock.accept()
        q.put((client_sock, client_addr))
 
 
echo_server(('', 15000), 128)

 

posted @   黄土地上的黑石头  阅读(316)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示