Python网络编程—socket(一)

    从今天开始python基础就介绍完毕了,下面我们将进阶到socket网络编程的介绍,那么socket是什么呢?我们带着这个问题开始今天的介绍:

  一、socket初探 

    socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求。

 socket其实也是一种特殊的文件,一些socket函数就是对其进行的操作(读/写、打开、关闭)

 那么socket对文件操作和file对文件操作有什么区别呢?

  • file模块是针对某个指定文件进行【打开】【读写】【关闭】

  • socket模块时针对服务器和客户端Socket进行【打开】【读写】【关闭】

 在介绍socket之前我们复习一下网络的OSI七层协议,方便后面的理解socket,请看下图:

 TCP/IP协议是主机接入互联网以及接入互联网的两台主机通信的标准,socket直接通信同样是遵循这个标准,下面介绍socket服务端和客户端直接是如何通信的,请看下图:

   下面我给大家解释一下上面这张图,我将socket之间的通信比作是电话之间通信,客户端和服务端通信必须有一个设备也就是手机或电话,然后服务端要绑定自己的手机卡并开机等待客户端打电话过来,客户端打电话过来,进行通话,这时就进入通话死循环直到双方挂电话。其实socket通信就好比打电话,首先实例化socket对象,调用socket对象中的方法,然后绑定本机的IP地址和端口,开启连接池listen(),然后阻塞accept(),直到客户端连接过来,客户端同样实例化socket对象,然后调用服务端通道conn()与服务端建立连接,这时客户端就和服务器端建立了连接,进入收发消息的死循环,直到有一方close()关闭连接,这就是整个socket的通信过程。

 下面来写一个单进程的socket通信,代码如下:

Socket_Server端:

 

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
import socket                #导入socket模块
 
ip_port=('127.0.0.1',9999)   #定义主机,端口号
 
s=socket.socket()            #实例化socket对象(买手机)
 
s.bind(ip_port)              #绑定IP端口(买手机卡)
 
s.listen(5)                  #建立5个连接池,等待接收请求,挂起连接
 
#等待电话
conn,addr=s.accept()         #conn就是建立通信,负责收发消息的通道,每次只处理一个请求,
                             #accept是阻塞请求,当第二个请求来的时候会进入listen连接池
                             #挂起等待处理
while True:
    try:
        recv_data = conn.recv(1024)             #收消息
        if len(recv_data) == 0:break            #如果客户输入为空的话退出
        if recv_data == 'exit':break            #客户端退出,服务端跟着退出
        send_data = recv_data.upper()          
 
        print(send_data)
        conn.send(send_data)                    #回复消息
    except Exception:
        break
 
conn.close()                                    #挂电话,关闭连接通道

Socket-Client端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
import socket
 
ip_port=('127.0.0.1',9999)
 
s=socket.socket()
 
s.connect(ip_port)    #这里的connect实际是服务端的conn,与服务端建立连接
 
 
while True:
    send_data=input(">>>: ").strip() #发送消息
    if send_data == 'exit':break
    if len(send_data) == 0:continue
    s.send(bytes(send_data,encoding='utf-8'))
 
    #收消息
    recv_data=s.recv(1024)
    print(str(recv_data,encoding='utf-8'))
 
#挂电话
s.close()

注意:1、基于python3.*版本的socket只能收发字节bytes(),python2.*版本是可以发送字符串str()的。

          2、s.accept()和s.recv()是阻塞的,前提必须基于连接正常的情况下,连接不正常就没有阻塞的情况发生。

 通过实验代码发现了一个bug,就是客户端退出,服务器端也跟着退出,那怎么让服务器端不退出呢?只要修改一个地方就可以了

 

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
import socket
 
ip_port=('127.0.0.1',9999)
 
s=socket.socket()
 
s.bind(ip_port)
 
s.listen(5)           #建立5个连接池,等待接收请求,挂起连接
 
while True:
    conn,addr=s.accept()
    while True:
        try:
            recv_data = conn.recv(1024)           
            if len(recv_data) == 0:break
            if recv_data == 'exit':break
            send_data = recv_data.upper()         
 
            print(send_data)
            conn.send(send_data)
        except Exception:
            break
 
    conn.close()

下面我们在举一个socket的例子,我们通过socket来写一个远程执行命令的程序:

Socket_server端:

 

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
#!/usr/bin/env python
# -*- coding:utf-8 -*-
 
import socket
import subprocess
ip_port=('127.0.0.1',9999)
 
s=socket.socket()
s.bind(ip_port)
s.listen(5)
 
while True:
    conn,addr=s.accept()
 
    while True:
            try:
                recv_data=conn.recv(1024)
                if len(recv_data) == 0:break
 
                p=subprocess.Popen(str(recv_data,encoding='utf8'),shell=True,stdout=subprocess.PIPE)                                                                             
                res=p.stdout.read()
                if len(res) == 0:
                    send_data='cmd err'
                else:
                    send_data=str(res,encoding='gbk')
 
                send_data=bytes(send_data,encoding='utf8')
                conn.send(send_data)
            except Exception:
                break
 
    conn.close()

Socket_Client端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
import socket
ip_port=('127.0.0.1',9999)
s=socket.socket()
 
s.connect(ip_port)
 
 
while True:
    send_data=input(">>>: ").strip()
    if send_data == 'exit':break
    if len(send_data) == 0:continue
    s.send(bytes(send_data,encoding='utf-8'))
 
    recv_data=s.recv(1024)
    print(str(recv_data,encoding='utf-8'))
 
s.close()

执行结果:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>>: ls
cmd err              #执行错误命令报错
>>>: dir
2016/07/04  23:49    <DIR>          .
2016/07/04  23:49    <DIR>          ..
2016/07/03  17:55               485 socket_client.py
2016/07/04  23:44               489 socket_client1.py
2016/07/04  23:34             1,088 socket_server.py
2016/07/04  23:49               937 socket_server_cmd.py
2016/07/03  17:58               650 thread socket server.py
 
>>>:ipconfig /all   
>>>:dir
#当我们执行完ipconfig /all命令后,再执行dir命令,还是显示的ipconfig /all的命令结果,这就涉及到了socket的一个粘包现象

大家想一下我们如何来解决socket的粘包现象,就是现在还无法判断客户端发来多少数据,导致一次无法全部接收完,请看下图:


就类似两个水桶注水一样,水桶A往水桶B注水,第一次注水10000毫升,而水桶B每次只能接收1024毫升,这时水桶A里又注入新的水,因为上次的10000毫升的水还没有完全注入水桶B中,所以还会接着注上次没有注完的水,直到将10000毫升的水注完为止,才会将新注入的水进入到B中。这就是socket粘包原理,那么怎么解决这种问题呢?

    试想我们每次将注入多少水告诉水桶B,然后让水桶B做好接收同样大小水的准备就可以解决问题,有多少接收多少,具体代码如下:

Socket_Server端:

 

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
#!/usr/bin/env python
# -*- coding:utf-8 -*-
 
import socket
import subprocess
ip_port=('127.0.0.1',9999)
 
s=socket.socket()          #绑定协议,生成套接字
s.bind(ip_port)            #绑定ip+协议+端口:用来唯一标识一个进程,ip_port必须是元组格式
s.listen(5)                #定义最大可以挂起的连接数
 
while True:                #用来重复接收新的链接
    conn,addr=s.accept()   #接收客户端胡链接请求,返回conn(相当于一个特定胡链接),addr是客户端ip+port
 
    while True:            #用来基于一个连接重复收发消息
            try:           #捕捉客户端异常关闭
                recv_data=conn.recv(1024)       #收消息,阻塞
                if len(recv_data) == 0:break    #客户端如果退出,服务端将收到空消息,退出
 
 
                p=subprocess.Popen(str(recv_data,encoding='utf8'),shell=True,stdout=subprocess.PIPE)
                #执行系统命令,windows平台命令的标准输出是gbk编码,需要转换
                res=p.stdout.read()     #获取标准输出
                if len(res) == 0:       #执行错误命令,标准输出为空,
                    send_data='cmd err'
                else:
                    send_data=str(res,encoding='gbk'#命令执行ok,字节gbk---->str---->字节utf-8
 
                send_data=bytes(send_data,encoding='utf8')
 
 
                #解决粘包问题
                ready_tag='Ready|%s' %len(send_data)
                conn.send(bytes(ready_tag,encoding='utf8')) #先将数据大小发送给客户端,让客户端等待接收
                feedback=conn.recv(1024)                    #接收客户端确认信息
                feedback=str(feedback,encoding='utf8')
 
                if feedback.startswith('Start'):
                    conn.send(send_data)                    #发送命令的执行结果
            except Exception:
                break
 
    conn.close()

Socket_Client端:

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
#!/usr/bin/env python
# -*- coding:utf-8 -*-
 
import socket
ip_port=('127.0.0.1',9999)
 
s=socket.socket()
 
s.connect(ip_port)      #链接服务端,如果服务已经存在一个好的连接,那么挂起
 
while True:             #基于connect建立的连接来循环发送消息
    send_data=input(">>: ").strip()
    if send_data == 'exit':break
    if len(send_data) == 0:continue
    s.send(bytes(send_data,encoding='utf8'))
 
    #解决粘包问题
    ready_tag=s.recv(1024)     #收取带数据长度的字节:Ready|9998
    ready_tag=str(ready_tag,encoding='utf8')
    if ready_tag.startswith('Ready'):           #判断是不是Ready开头的
        msg_size=int(ready_tag.split('|')[-1])  #获取待接收数据长度
    start_tag='Start'
    s.send(bytes(start_tag,encoding='utf8'))    #客户端已经准备好,发送确认信息
 
    #基于已经收到的待接收数据长度,循环接收数据
    recv_size=0
    recv_msg=b''
    while recv_size < msg_size:
        recv_data=s.recv(1024)
        recv_msg+=recv_data
        recv_size+=len(recv_data)
        print('MSG SIZE %s RECE SIZE %s' %(msg_size,recv_size))
 
    print(str(recv_msg,encoding='utf8'))
 
s.close()

上面是举的几个例子,下面介绍一下socket对象的一些功能:

     1, sk=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

 

1
2
3
4
5
6
7
8
参数一:地址簇
    socket.AF_INET     #IPv4(默认)
    socket.AF_INET6    #IPv6
    socket.AF_UNIX     #只能够用于单一的UNIX系统几件通信
 
参数二:类型
    socket.SOCK_STREAM   #流式socket,for TCP(默认)
    socket.SOCK_DGRAM    #数据报式,for UDP

    2,sk.bind(address)

s.bind(address):将套接字绑定到地址。address地址的格式取决于地址簇。在AF_INET下,以元组(host,port)的形式表示地址。

    3,sk.listen(backlog)

开始监听传入连接。backlog指定在拒绝连接之前,可以挂起的最大连接数量。

backlog等于5,表示内核已经接到了连接请求,但服务器还没有调用accept进行处理的连接个数最大为5,这个值不能无限大,因为要在内核中维护连接队列。

    4,sk.setbloking(bool)

是否阻塞(默认为True),如果设置False,那么accept和recv时一旦无数据,则报错。

    5,sk.accept()

接受连接并返回(conn,address),其中conn是新的套接字对象,可以用来接收和发送数据;address是连接客户端的地址,接收TCP客户的连接(阻塞式)等待连接的到来。

    6,sk.connect(address)

连接到address处的套接字。一般address的格式为元组(hostname,port),如果连接出错,返回socket.error错误。

    7,sk.connect_ex(address)

同上,只不过会有返回值,连接成功时返回0,连接失败时候返回编码,例如:10061

    8,sk.close()

 关闭套接字

    9,sk.recv(bufsize)

 接受套接字的数据,数据以字节形式返回,bufsize指定最多可以接收的数量。

   10,sk.send(bytes)

 将字节数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。即:可能未将指定内容全部发送。

    11,sk.sendall(bytes)

 

将字节的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。

内部通过递归调用send,将所有内容发送出去。

    12,sk.settimeout(timeout)

设置套接字操作的超时时间,timeout是一个浮点数,单位是秒。值为None表示没有超时期,一般超时期应该在刚创建套接字时设置,因为他们可能用于连接的操作(如client连接最多等待5s)

 socket的功能就介绍到这里,更多方法请参考源码。

 二、SocketServer模块

      通过上面的例子我们可以发现我们的程序,都是单进程来工作,服务端一次只能处理一个请求,然而现实生产的情况下,是要支持多并发的连接,下面我们就介绍socket如何处理并发请求。

SocketServer内部使用IO多路复用以及"多线程","多进程",从而实现并发处理多个客户端请求的Socket服务端。即:每个客户端请求连接服务器时,Socket服务端都会在服务器上创建一个"线程"或者"进程"专门负责处理当前客户端的所有请求。

Threading TCPServer:ThreadingTCPServer实现的Socket服务器内部会为每个client创建一个"线程",该线程用来和客户端进行交互。

1、ThreadingTCPServer基础

如何使用ThreadingTCPServer:

  • 创建一个继承自SocketServer.BaseRequestHandler的类;

  • 类中必须定义一个名称为handle的方法;

  • 启动ThreadingTCPServer。

请看下面的例子:

Socket_Server端:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
import socketserver      
 
class MyServer(socketserver.BaseRequestHandler):
    def handle(self):
        #print self.request,self.client_address,self.server
        self.request.sendall(bytes('hello,world!!',encoding='utf-8'))
        while True:
            data = self.request.recv(1024)
            if len(data) ==0:break
            print("[%s] says %s"%(self.client_address,data.decode() ))
            self.request.sendall(data.upper())
 
 
if __name__=='__main__':
    server=socketserver.ThreadingTCPServer(('127.0.0.1',9999),MyServer)
    server.serve_forever()

Socket_Client端:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
import socket
ip_port=('127.0.0.1',9999)
 
s=socket.socket()
 
s.connect(ip_port)
 
welcome_msg = s.recv(1024)
print("from server:",welcome_msg.decode())
while True:
    send_data=input(">>: ").strip()
    if len(send_data) == 0:continue
    s.send(bytes(send_data,encoding='utf8'))
 
    recv_data=s.recv(1024)
    print(str(recv_data,encoding='utf8'))
 
s.close()

2、ThreadingTCPServer源码剖析

 

 内部调用流程为:

  • 启动服务端程序;

  • 执行TCPServer.__init__方法,创建服务端Socket对象并绑定IP和端口;

  • 执行BaseServer.__init__方法,将自定义的继承自SocketServer.BaseRequestHandler的类MyRequestHandle赋值给self.RequestHandlerClass;

  • 执行BaseServer.server_forever方法,While循环一直监听是否有客户端请求到达;

  • 当客户端请求到达服务器;

  • 执行ThreadingMixIn.process_request方法,创建一个"线程"用来处理请求;

  • 执行ThreadingMixIn.process_request_thread方法;

  • 执行BaseServer.finish_request方法,执行self.RequestHandlerClass(),即:执行自定义MyRequestHandler的构造方法(自动调用基类BaseRequestHandler的构造方法,在该构造方法中又会调用MyRequestHandler的Handle方法)

​大概就是这个执行过程,下面在我们将上面的远程执行命令的脚本做些修改让它可以处理多并发的请求:

 

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
#!/usr/bin/env python
# -*- coding:utf-8 -*-
 
import socketserver
import subprocess
class MyServer(socketserver.BaseRequestHandler):
    def handle(self):
        # print self.request,self.client_address,self.server
        self.request.sendall(bytes('欢迎致电 10086,请输入1xxx,0转人工服务.',encoding="utf-8"))
        while True:
            data = self.request.recv(1024)
            if len(data) == 0:break
            print("[%s] says:%s" % (self.client_address,data.decode() ))
            cmd = subprocess.Popen(data.decode(),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
            cmd_res =cmd.stdout.read()
            if not cmd_res:
               cmd_res = cmd.stderr.read()
            if len(cmd_res) == 0: #cmd has not output
               cmd_res = bytes("cmd has output",encoding="utf-8")
            self.request.send(cmd_res )
 
 
if __name__ == '__main__':
    server = socketserver.ThreadingTCPServer(('0.0.0.0',8009),MyServer)
    server.serve_forever()

下面将今天介绍的内容都串起来写个小程序,我们大家都使用过FTP来上传和下载文件,现在我们尝试自己来写一个FTP的功能,这里只实验一个FTP上传的功能:

Socket_Server端:

 

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
#!/usr/bin/env python
# -*- coding:utf-8 -*-
 
import socketserver,json
class MyServer(socketserver.BaseRequestHandler):
    def handle(self):
        # print self.request,self.client_address,self.server
        self.request.sendall(bytes('欢迎致电 10086,请输入1xxx,0转人工服务.',encoding="utf-8"))
        while True:
            data = self.request.recv(1024)
            if len(data) == 0:break
            print("data", data)
            print("[%s] says:%s" % (self.client_address,data.decode() ))
 
            task_data = json.loads( data.decode()  )
            task_action = task_data.get("action")
            if hasattr(self, "task_%s"%task_action):
               func = getattr(self,"task_%s" %task_action)
               func(task_data)     
            else:
               print("task action is not supported",task_action)
 
    def task_put(self,*args,**kwargs):
        print("---put",args,kwargs)        
        filename = args[0].get('filename')
        filesize = args[0].get('file_size')
        server_response = {"status":200}
        self.request.send(bytes( json.dumps(server_response), encoding='utf-8'  ))
        f = open(filename,'wb')
        recv_size = 0
        while recv_size < filesize:
            data = self.request.recv(4096)
            f.write(data)
            recv_size += len(data)
            print('filesize: %s  recvsize:%s' % (filesize,recv_size))
        print("file recv success")
        f.close()
 
if __name__ == '__main__':
    server = socketserver.ThreadingTCPServer(('0.0.0.0',8009),MyServer)
    server.serve_forever()

Socket_Client端:

 

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
#!/usr/bin/env python
# -*- coding:utf-8 -*-
 
 
import socket
import os ,json
ip_port=('192.168.11.150',8009)
 
s=socket.socket()
 
s.connect(ip_port)
 
welcome_msg = s.recv(1024)
print("from server:",welcome_msg.decode())
while True:
    send_data=input(">>: ").strip()
    if len(send_data) == 0:continue
 
    cmd_list = send_data.split()
    if len(cmd_list) <2:continue
    task_type = cmd_list[0]
    if task_type == 'put':
        abs_filepath = cmd_list[1]
        if os.path.isfile(abs_filepath):
            file_size = os.stat(abs_filepath).st_size
            filename = abs_filepath.split("\\")[-1]
            print('file:%s size:%s' %(abs_filepath,file_size))
            msg_data = {"action":"put",
                        "filename":filename,
                        "file_size":file_size}
 
            s.send(  bytes(json.dumps(msg_data),encoding="utf-8")  )
            server_confirmation_msg = s.recv(1024)
            confirm_data = json.loads(server_confirmation_msg.decode())
            if confirm_data['status'] ==200:
 
                print("start sending file ",filename)
                f = open(abs_filepath,'rb')
                for line in f:
                    s.send(line)
 
                print("send file done ")
 
        else:
            print("\033[31;1mfile [%s] is not exist\033[0m" % abs_filepath)
            continue
    else:
        print("doesn't support task type",task_type)
        continue
    recv_data=s.recv(1024)
    print(str(recv_data,encoding='utf8'))
 
s.close()

在这里补充一个如何在终端上打印进度条的代码,供大家参考:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python
#-*- coding:utf-8-*-
 
import time
import sys
 
def progress_test():
    bar_length = 20
    for percent in range(0, 101):
        hashes = '#' * int(percent / 100.0 * bar_length)
        spaces = ' ' * (bar_length - len(hashes))
        sys.stdout.write("\rPercent: [%s] %d%%" % (hashes + spaces, percent))
 
        sys.stdout.flush()
        time.sleep(0.1)
 
progress_test()

今天的socket就介绍到这里,后续会有Socket的进阶,请大家关注。

 

posted @ 2016-07-05 23:06  邸海峰  阅读(753)  评论(0编辑  收藏  举报
doc