首页 > 技术文章 > Python socket编程

nopnog 2017-06-26 17:18 原文

本章内容

  1、Socket简介

  2、Socket远程服务器操作

  3、SocketServer模块

  4、粘包

 

Socket简介

  python内有很多针对常见网络协议的库,

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

  socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,对于文件用【打开】【读写】【关闭】模式来操作。socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)

  socket和file的区别:

  • file模块是针对某个指定文件进行【打开】【读写】【关闭】
  • socket模块是针对 服务器端 和 客户端Socket 进行【打开】【读写】【关闭】

  sample

import socket


#第一步相当于买手机,    sockect家族    ,tcp
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

#买完手机后插入手机卡,绑定ip,确定服务的唯一性
phone.bind(('127.0.0.1',8080))

#手机开机,并开5个进程,来处理问题
phone.listen(5)

#接电话,且里面有两个值,
conn,addr = phone.accept()

#接受消息,大小1024
data = conn.recv(1024)

print('from client msg %s'%dats)

conn.send(data.upper())

cnn.close() #断链接
phone.close() #关闭socket
服务端配置
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

client.connect(('127.0.0.1',8080))

client.send('hello'.encode('utf-8'))

data = client.recv(1024)

client.close()
客户端配置

  server = socket.socket(socket.AF_INET,socket.SOCK_STREAM,0)          #括号内的内容不填写,默认是这些。

  参数一:

    socket.AF_INET        IPv4       (默认)

    socket.AF_INET6      IPv6

    socket.AF_UNIX       只能够用于单一的Unix系统进程间通信

  参数二:

    socket.SOCK_STREAM  流式socket , for TCP (默认)
    socket.SOCK_DGRAM   数据报式socket , for UDP

    socket.SOCK_RAW 原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。
    socket.SOCK_RDM 是一种可靠的UDP形式,即保证交付数据报但不保证顺序。SOCK_RAM用来提供对原始协议的低级访问,在需要执行某些特殊操作时使用,如发送ICMP报文。SOCK_RAM通常仅限于高级用户或管理员运行的程序使用。
    socket.SOCK_SEQPACKET 可靠的连续数据包服务

  参数三:

    0  (默认)与特定的地址家族相关的协议,如果是 0 ,则系统就会根据地址格式和套接类别,自动选择一个合适的协议

  server.bind(address)

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

  server.listen(backlog)

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

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

  server.setblocking(bool)

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

  server.accept()

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

    接收TCP 客户的连接(阻塞式)等待连接的到来

  server.connect(address)

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

  server.connect_ex(address)

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

  server.close()

    关闭套接字

  server.recv(bufsize[,flag])

    接受套接字的数据。数据以字符串形式返回,bufsize指定最多可以接收的数量。flag提供有关消息的其他信息,通常可以忽略。

  server.recvfrom(bufsize[.flag])

    与recv()类似,但返回值是(data,address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址。

  server.send(string[,flag])

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

  server.sendall(string[,flag])

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

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

  server.sendto(string[,flag],address)

    将数据发送到套接字,address是形式为(ipaddr,port)的元组,指定远程地址。返回值是发送的字节数。该函数主要用于UDP协议。

  server.settimeout(timeout)

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

  server.getpeername()

    返回连接套接字的远程地址。返回值通常是元组(ipaddr,port)。

  server.getsockname()

    返回套接字自己的地址。通常是一个元组(ipaddr,port)

  server.fileno()

    套接字的文件描述符

 

  升级版: 

#可实现不停的收发数据

import socket

#第一步相当于买手机,    sockect家族    ,tcp
phone = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

#买完手机后插入手机卡,绑定ip,确定服务的唯一性
phone.bind(('127.0.0.1',8080))

#手机开机,并开5个进程,来处理问题
phone.listen(5)

while True:
    #接电话,且里面有两个值,
    conn,addr = phone.accept()

    while True:
        try:
            #接受消息,大小1024
            data = conn.recv(1024)
            if not data:     #如果内容为空
                break

            print('from client msg %s'%dats)

            conn.send(data.upper())
        except:
            break

    cnn.close() #断链接
phone.close() #关闭socket
服务端配置
import socket

ip_port = ('127.0.0.1',9998)

client = socket.socket()
client.connect(ip_port)

while True:
    send_data = input('whaht do you want to send? =>')
    client.sendall(send_data.encode('utf-8'))
    server_reply = client.recv(1024)
    print('the data from server',server_reply)

client.close()
客户端配置

 

Socket远程服务器操作

import socket
import os

ip_port = ('127.0.0.1',9983)

server = socket.socket()
server.bind(ip_port)
server.listen(5)

while True:
    print('wait to connect ....')
    conn,addr = server.accept()

    while True:
        print('wait for recv command ....')
        client_data = conn.recv(1024)
        print('the command from client...',client_data)
        res = os.popen(client_data.decode('utf-8')).read()
        print(res)
        conn.send(res.encode('utf-8'))     #起先这里没有转码为bytes,会有报错

    conn.close()
服务器端
import socket

ip_port = ('127.0.0.1',9983)

client = socket.socket()
client.connect(ip_port)

while True:
    send_data = input('whaht do you want to send? =>')
    client.sendall(send_data.encode('utf-8'))
    server_reply = client.recv(1024)
    print(server_reply)

client.close()
客户端

  运行中的问题,命令接受过来是bytes类型,命令运行的话需要decode,而send发送的时候需要bytes,所以发送的时候需要decode,不然的话会有TypeError:'str' does not support the buffer interface

   客户端接受到数据,因为是bytes类型,显示中文需要decode()下

 

  提前告知client端文件解决粘包问题

import socket
import os

ip_port = ('127.0.0.1',9999)

server = socket.socket()
server.bind(ip_port)
server.listen(5)

while True:
    print('等待链接 ....')
    conn,addr = server.accept()

    while True:
        print('等待执行命令 ....')
        client_data = conn.recv(1024)
        print('client_data',client_data)
        if not client_data:
            print('客户端已经断开')
            break
        print('执行命令...',client_data)
        res = os.popen(client_data.decode('utf-8')).read()
        print('发送数据之前...')
        if len(res) ==0:
            res = 'there is no this data!'
        conn.send(str(len(res.encode('utf-8'))).encode("utf-8"))
        # print('res',res)
        conn.send(res.encode('utf-8'))
        print('发送命令done')

    conn.close()

server.close()
server端
import socket

ip_port = ('127.0.0.1',9999)

client = socket.socket()
client.connect(ip_port)

while True:
    send_data = input('what do you want to send? =>')
    client.sendall(send_data.encode('utf-8'))
    server_body_size = client.recv(1024)             #告知客户端需要接收文件的大小
    print('需要接受的文件大小:',server_body_size)

    receive_size = 0
    receive_data = b''
    n = 1

    while receive_size < int(server_body_size.decode()):
        print('第%s循环'%n)
        data = client.recv(1024)
        receive_size +=len(data)
        receive_data += data

        n +=1

    print('接收了数据的大小',receive_size)
    print(receive_data.decode())
    print('接受数据done')

client.close()
客户端

  如果把这些代码放到linux上面执行,则会有一个问题,会有个报错,send需要发的包的大小,会和send包的内容粘到一会来发送,这就是所谓的粘包

  解决办法:

  1、两个send之间sleep(0.5) 会解决这个问题,不过这个很low,不能时时获取数据

  2、这个方法就是在server端send完数据后,然后再cnn.recv()等待接受数据,在client端则是,接收到server端发来的文件大小长度后,然后再随便send点东西给server端,server端再执行发送正常数据的命令,这个问题就解决了。

 

  到server,get一个文件下载到本地

import socket
import os
import hashlib

ip_port = ('127.0.0.1',9995)

server = socket.socket()
server.bind(ip_port)
server.listen(5)

while True:
    print('等待链接 ....')
    conn,addr = server.accept()

    while True:
        print('等待执行命令 ....')
        client_data = conn.recv(1024)
        print('接收到的命令文件',client_data)
        client_method,client_file = client_data.split()
        print(client_method,client_file)
        if os.path.isfile(client_file):
            print('条件命令执行通过')
            file_size = os.stat(client_file).st_size          #获取本地文件的大小,这个点新get到的
            conn.send(str(file_size).encode('utf-8'))
            print('发送文件的大小给客户端', file_size)
            conn.recv(1024)
            print('收到客户端响应已经收到文件大小')
            md5 = hashlib.md5()
            f = open(client_file,'rb')
            for line in f:
                conn.send(line)
                md5.update(line)
            print('发送数据结束')
            conn.send(md5.hexdigest().encode('utf-8'))
            print('md5已发送给客户端')

    conn.close()

server.close()
server端
import socket
import hashlib

ip_port = ('127.0.0.1',9995)

client = socket.socket()
client.connect(ip_port)

while True:
    send_data = input('what do you want to send? =>')
    if send_data.startswith('get'):
        want_file = send_data.split()[1]
        client.send(send_data.encode('utf-8'))
        want_file_size = client.recv(1024)
        print('接收到的文件大小',want_file_size)
        want_file_size = int(want_file_size.decode())
        client.send(b'Got the file_size')
        print('回复确认已经收到文件的大小')
        md5 = hashlib.md5()
        f = open(want_file + '.new', 'wb')          #这个更改文件名字的方式新get到
        get_file_size = 0

        while get_file_size < want_file_size:
            if want_file_size - get_file_size >= 1024:
                size = 1024
            else:
                size = want_file_size - get_file_size

            data = client.recv(size)
            get_file_size += len(data)
            print(want_file_size,get_file_size,size)
            f.write(data)
            md5.update(data)
        else:
            f.close()
            print('客户端文件接收完毕')
            server_file_md5 = client.recv(1024)
            print('服务端md5',server_file_md5)
            print('客户端接收MD5',md5.hexdigest())

client.close()
View Code

 

SocketServer模块

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

    

  ThreadingTCPServer

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

  1、ThreadingTCPServer基础

  使用ThreadingTCPServer:

  • 创建一个继承自 SocketServer.BaseRequestHandler 的类
  • 类中必须定义一个名称为 handle 的方法
  • 启动ThreadingTCPServer

  sample:

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import SocketServer

class MyServer(SocketServer.BaseRequestHandler):

    def handle(self):
        # print self.request,self.client_address,self.server
        conn = self.request
        conn.sendall('欢迎致电 10086,请输入1xxx,0转人工服务.')
        Flag = True
        while Flag:
            data = conn.recv(1024)
            if data == 'exit':
                Flag = False
            elif data == '0':
                conn.sendall('通过可能会被录音.balabala一大推')
            else:
                conn.sendall('请重新输入.')


if __name__ == '__main__':
    server = SocketServer.ThreadingTCPServer(('127.0.0.1',8009),MyServer)
    server.serve_forever()

    2、ThreadingTCPServer源代码(待研究)

  ForkingTCPServer  

    ForkingTCPServer和ThreadingTCPServer的使用和执行流程基本一致,只不过在内部分别为请求者建立 “线程”  和 “进程”。   

  sample:  

复制代码
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import SocketServer

class MyServer(SocketServer.BaseRequestHandler):

    def handle(self):
        # print self.request,self.client_address,self.server
        conn = self.request
        conn.sendall('欢迎致电 10086,请输入1xxx,0转人工服务.')
        Flag = True
        while Flag:
            data = conn.recv(1024)
            if data == 'exit':
                Flag = False
            elif data == '0':
                conn.sendall('通过可能会被录音.balabala一大推')
            else:
                conn.sendall('请重新输入.')


if __name__ == '__main__':
    server = SocketServer.ForkingTCPServer(('127.0.0.1',8009),MyServer)    #与线程只有这里不相同
    server.serve_forever()

  SocketServer的ThreadingTCPServer之所以可以同时处理请求得益于 select 和 os.fork 两个东西,其实本质上就是在服务器端为每一个客户端创建一个进程,当前新创建的进程用来处理对应客户端的请求,所以,可以支持同时n个客户端链接(长连接)。

    

  上传一个文件到服务器端(面向对象的方式,socketserver)

import socketserver
import json
import os


class MyTcpHandler(socketserver.BaseRequestHandler):
    def put(self,*args):
        cmd_dict = args[0]
        filename = cmd_dict['filename']
        filesize = cmd_dict['size']
        if os.path.isfile(filename):
            f = open(filename + '.new','wb')
        else:
            f = open(filename,'wb')

        self.request.send(b'200,ok')
        received_size = 0
        while received_size < filesize:
            data = self.request.recv(1024)
            f.write(data)
            received_size += len(data)
            print(received_size,filesize)

        else:
            print('%s文件接收完毕'%filename)

    def handle(self):
        while True:
            try:
                self.data = self.request.recv(1024).strip()
                print("{} wrote:".format(self.client_address[0]))
                print('在客户端收到接受文件前的信息',self.data)
                cmd_dict = json.loads(self.data.decode('utf-8'))
                print('在客户端接收到的信息是:',cmd_dict)
                action = cmd_dict['action']
                if hasattr(self,action):               #反射方法
                    func = getattr(self,action)
                    func(cmd_dict)

            except ConnectionResetError as e:
                print('error',e)
                break

if __name__ == '__main__':
    HOST,PORT = 'localhost',9997
    server = socketserver.ThreadingTCPServer((HOST,PORT),MyTcpHandler)
    print('ftpserver已启动')
    server.serve_forever()
server端
import json
import socket
import os



class FtpClient(object):
    def __init__(self):
        self.client = socket.socket()

    def help(self):
        msg = '''
                ls
                pwd
                cd ../..
                get filename
                put filename
                '''
        print(msg)

    def connect(self,ip,port):
        self.client.connect((ip,port))
        print('已连接到ftpserver')

    def interactive(self):
        while True:
            cmd = input('input you cmd=>').strip()
            if len(cmd) == 0:continue

            cmd_str = cmd.split()[0]
            if hasattr(self,'cmd_%s'%cmd_str):        #反射,判断有没有这个方法
                func = getattr(self,"cmd_%s"%cmd_str) #反射,执行这个方法
                func(cmd)
                print('下一步运行put方法')

            else:
                self.help()

    def cmd_put(self,*args):
        print('开始上传文件......')
        cmd_method = args[0].split()
        if len(cmd_method) >1:
            filename = cmd_method[1]
            if os.path.isfile(filename):
                filesize = os.stat(filename).st_size
                mes_dict = {
                    'action':'put',
                    'filename':filename,
                    "size":filesize,
                    "overridden":True
                }
                self.client.send(json.dumps(mes_dict).encode('utf-8'))
                print('发送了文件信息',json.dumps(mes_dict).encode("utf-8"))
                server_response = self.client.recv(1024)
                print('收到了ftp端的确认信息:',server_response)

                f = open(filename,'rb')
                for line in f:
                    self.client.send(line)
                else:
                    print('发送文件完毕...')
                    f.close()
            else:
                print('需要发送的文件不存在')

    def cmd_get(self):
        pass

ftp = FtpClient()
ftp.connect('localhost',9997)
ftp.interactive()
client 端

 

粘包

  期间你会遇到粘包的问题所谓的粘包的问题是指,你这次输入的命令,返回的结果却是上一次的输入命令的结果问题的原因在于,接收方不知道消息的大小,不知道一次要取多少造成的,tcp有这样的问题,udp没有,程序会有个缓存区

 

        如图所示,程序每次发送消息不是直接给客户,而是放到缓冲区,接受消息也是如此,在缓冲区去取。tcp是流式的,会一直发,信息都存在客户短的缓存中,如果第一次取消息设置的不够大,那就取不完,下一次再输入新的命令时,会继续取上次留下的消息,这就是造成粘包的原因

  那如何解决呢?

  办法是,服务端在发送消息之前告知客户端我这信息的大小,然后客户端用这个大小去接受,这样每次接受的就是完整的信息了,代码实现如下

      客户端的处理

import struct
#struct 这个模块是用来计算数据大小的
data=client。recv(4) #struct 计算后得知消息体的大小,这段占位为4
data_size=struct.unpack('i',data)[0]#获得需要发送消息的大小

res=client.revc(data_size)#接受实际的大小
print (res.decode(‘gbk’))

  服务端处理

import struct

conn.send(struct.pack('i',len(back_msg))) #i,打包的模式,len是消息的大小。
conn.send(back_msg) #在发送消息

 

  这里有个问题,如果数据过大,比缓存区还大,直接send的话数据就会没了,这是后需要用

    conn.sendall(back_msg)

    sendall是循环调用send命令。

客户端收的时候的处理,

recv_size=0
recv_bytes=b''
while recv_size < data_size:
res=client.recv(1024)
recv_bytes+=res
recv_size+=len(res)

print(recv_bytes.decode('gbk'))

      如果,再大,大到文件的长度4个比特位都放不下那有如何,这里就引用到了字典-json,先把文件存为字典{’size‘:1234567891231456789123456789}然后再json化传递给客户端,客户端在反json后再读取,如此获得文件的大小

  服务端的配置

import json
head_dict={'data_size':len(back_msg)}#将文件大小放到字典中
head_json=json.dumps(head_dict) #json化
head_bytes=head_json.encode('utf-8')


conn.send(struct.pack('i',len(head_bytes)))#先发送这个json的大小


conn.send(head_bytes)#发送json文件


conn.sendall(back_msg)#发送真实的文件

   客户端的配置

#收取包头的长度
head=client.recv(4)
head_size=struct.unpack('i',head)[0]

#收取包头
head_bytes=client.recv(head_size)
head_json=head_bytes.decode('utf-8')
head_dict=json.loads(head_json)
data_size=head_dict['data_size']

#收取真实的数据:
recv_size=0
recv_bytes=b''
while recv_size < data_size:
res=client.recv(1024)
recv_bytes+=res
recv_size+=len(res)

print(recv_bytes.decode('gbk'))

 

 

 

返回python目录

 

推荐阅读