Python 网络编程(5):基于socket的网络编程

网络通信其实就是Socket间的通信

“两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。”

可以这么说,Socket就是一个网络编程的接口(API),它定义了一种标准,并对TCP/IP进行封装,实现了网络传输数据的能力。

1. socket 小结

1.1 套接字的三个属性

套接字的特性由3个属性确定,它们分别是:端口号协议类型

  • 套接字的AF_INETAF_UNIX
    前者指的是Internet网络,后者表示UNIX文件系统
  • 套接字的端口号:范围是0-65535
  • 套接字协议类型SOCK_STREAMSOCK_DGRAMSOCKET_RAW(原始套接字)
    SOCK_STREAM(流套接字)在域中通过TCP/IP连接实现,同时也是AF_UNIX中常用的套接字类型;
    SOCK_DGRAM(数据报套接字)在域中通常是通过UDP/IP协议实现;
    SOCKET_RAW(原始套接字)允许对较低层次的协议直接访问,比如IP、 ICMP协议

1.2 socket 通信基本流程图

在这里插入图片描述

在这里插入图片描述

1.3 TCP

TCP 网络编程

TCP(Transmission Control Protocol,传输控制协议)是基于连接的协议,也就是说,在正式收发数据前必须和对方建立可靠的连接

在这里插入图片描述
首先,客户端和服务端会分别新建一个socket,服务端的socket需要通过bind()来绑定上端口,启动listen()进行实时监听,并等待客户端的接入,即accept()。而客户端则需要通过服务器IP和端口两个参数来建立connect()连接,此时,服务器会得到有新客户端连接的信息,启动read()等待客户端数据的传人,客户端如果成功接收到服务端的连接成功后,继续执行write()来向服务端发生数据,同理,服务端也使用这样的模式回馈客户端的数据,知道客户端关闭,服务端会收到客户端退出连接的消息,服务器重新进入等待状态,等待新客户端的进入。

在这里插入图片描述

Python代码:

  • 服务端

    import socket
    #服务端
    new_socket = socket.socket()         # 创建 socket 对象
    ip = "127.0.0.1"          # 获取本地主机名
    port = 52052                # 设置端口
    new_socket.bind((ip, port))        # 绑定端口
    new_socket.listen(5)                 # 等待客户端连接并设置最大连接数
    while True:
        new_cil, addr = new_socket.accept()     # 建立客户端连接。
        print('新进来的客户端的地址:', addr)
        print(new_cil.recv().decode())
        new_cil.send('答案为6')
        new_cil.close()                # 关闭连接
    
  • 客户端

    import socket
    #客户端
    ip = "127.0.0.1"
    port = 52052
    new_socket = socket.socket()  #创建socket对象
    new_socket.connect((ip,port))  #连接
    new_socket.send("请求给我计算下1+5=多少?".encode(encoding='utf-8')) #发生数据
    print("客户端发给服务端:请求给我计算下1+5=多少?") 
    back_str = new_socket.recv().decode() #结束数据
    print("服务端发给客户端:"+back_str)
    new_socket.close() #关闭客户端
    print("客户端结束运行")
    

小坑:encode() 和 decode()

关于 encode() 和 decode():

  • encode()编码str -> bytes,str.encode()
  • decode()解码bytes -> str,bytes.decode()

而在 socket 编程中:

  • socket.send(bytes)
  • socket.recv(bytes),注意:其结果是str格式!!
  • print(str)

因此注意数据格式的转换!!!

1.4 UDP(暂略)

2. Python socket 类

这个Python接口是用Python的面向对象风格对Unix系统调用和套接字库接口的直译:函数 socket() 返回一个 套接字对象 ,其方法是对各种套接字系统调用的实现。

参见:

  • 模块:socketserver
    用于简化网络服务端编写的类。
  • 模块:ssl
    套接字对象的TLS/SSL封装。

socket 实例类:socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)

注:
proto=0 请忽略,特殊用途
fileno=None 请忽略,特殊用途

family(socket家族)

  • socket.AF_UNIX:用于本机进程间通讯,为了保证程序安全,两个独立的程序(进程)间是不能互相访问彼此的内存的,但为了实现进程间的通讯,可以通过创建一个本地的socket来完成

  • socket.AF_INET:表示ipv4,还有AF_INET6被用于ipv6。

  • 所有地址家族中,AF_INET是使用最广泛的一个,python支持很多种地址家族,但是由于我们只关心网络编程,所以大部分时候我么只使用AF_INET

socket type类型

  • socket.SOCK_STREAM # for tcp
  • socket.SOCK_DGRAM # for udp

服务端套接字函数

  • s.bind() 绑定(主机,端口号)到套接字
  • s.listen() 开始TCP监听
  • s.accept() 被动接受TCP客户的连接,(阻塞式)等待连接的到来

客户端套接字函数

  • s.connect() 主动初始化TCP服务器连接
  • s.connect_ex() connect()函数的扩展版本,出错时返回出错码,而不是抛出异常

公共用途的套接字函数

  • s.recv() 接收数据
  • s.send() 发送数据(send在待发送数据量大于己端缓存区剩余空间时,数据丢失,不会发完,可后面通过实例解释)
  • s.sendall() 发送完整的TCP数据(本质就是循环调用send,sendall在待发送数据量大于己端缓存区剩余空间时,数据不丢失,循环调用send直到发完)
  • s.recvfrom() Receive data from the socket. The return value is a pair (bytes, address)
  • s.getpeername() 连接到当前套接字的远端的地址
  • s.close() 关闭套接字
  • socket.setblocking(flag) #True or False,设置socket为非阻塞模式,以后讲io异步时会用
  • socket.getaddrinfo(host, port, family=0, type=0, proto=0, flags=0) 返回远程主机的地址信息,例子 socket.getaddrinfo(‘luffycity.com’,80)
  • socket.getfqdn() 拿到本机的主机名
  • socket.gethostbyname() 通过域名解析ip地址

3. 小结

总结

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

附:粘包

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

接收方不知道接受消息的界限(可能没有接受完,留在管道里,下次会继续接受,和后面的黏在一起了)。另一方面是,底层优化算法,将时间间隔短,小的数据包合成一个包发送。接收端不知道是哪一次的,小的数据就会一次接受出现粘包。

3. 应用

3.1 基于socket开发一个聊天程序,实现两端互相发送和接收消息

使用:先打开 server 端,再再另一个窗口打开 client 端。

  • client

    import socket
      
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    phone.connect(('127.0.0.1', 8083))
    
    while True:
        msg = input('>>>: ').strip() # msg=''
        if not msg:continue
        phone.send(msg.encode('utf-8')) # phone.send(b'')
    
        data = phone.recv(1024)
    
        print(data.decode('utf-8'))
    
    phone.close()
    
  • server

    import socket
      
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    phone.bind(('127.0.0.1', 8083)) #0-65535:0-1024给操作系统使用
    phone.listen(5)
    
    print('starting...')
    while True: # 连接循环
        conn, client_addr = phone.accept()
        print(client_addr)
    
        while True: # 通讯循环
            try:
                data = conn.recv(1024).decode('utf-8') # decode('utf-8')支持显示中文
                if not data:break #适用于linux操作系统
                print('From client:', data)
                response = input('>>>:').strip()
                conn.send(response.encode())
            except ConnectionResetError: #适用于windows操作系统
                break
        conn.close()
    
    phone.close()
    

效果:

  • server
    在这里插入图片描述

  • client
    在这里插入图片描述

3.2 基于tcp socket,开发简单的远程命令执行程序,允许用户执行命令,并返回结果

  • server
import socket
import subprocess
import struct
import json

phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.bind(('127.0.0.1',9909)) #0-65535:0-1024给操作系统使用
phone.listen(5)

print('starting...')
while True: # 链接循环,保证客户端断开时,服务端不断开
    conn,client_addr=phone.accept()
    print(client_addr)

    while True: #通信循环
        try:
            #1、收命令
            cmd=conn.recv(8096)
            if not cmd:break #适用于linux操作系统

            #2、执行命令,拿到结果
            obj = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)

            stdout=obj.stdout.read()
            stderr=obj.stderr.read()

            #3、把命令的结果返回给客户端
            #第一步:制作固定长度的报头(包含数据的长度)
            header_dic={
                'filename':'%s.txt'%cmd,
                'md5':'xxdxxx',
                'total_size': len(stdout) + len(stderr)
            }
            header_json=json.dumps(header_dic)
            header_bytes=header_json.encode('utf-8')

            #第二步:利用struct后,将处理后的报头长度发送(这样报头长度固定的)
            conn.send(struct.pack('i',len(header_bytes)))
            #json处理后,不用担心数据长度过长,导致使用struct出错

            #第三步:再发报头(包含数据的长度)
            conn.send(header_bytes)

            #第四步:再发送真实的数据(直接发数据)
            conn.send(stdout)
            conn.send(stderr)

        except ConnectionResetError: #适用于windows操作系统
            break
    conn.close()

phone.close()
  • client
import socket
import struct
import json

phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

phone.connect(('127.0.0.1',9909))

while True:
    #1、发命令
    cmd=input('>>: ').strip() #ls /etc
    if not cmd:continue
    phone.send(cmd.encode('utf-8'))

    #2、拿命令的结果,并打印

    #第一步:先收报头的长度
    obj=phone.recv(4)
    header_size=struct.unpack('i',obj)[0]

    #第二步:再收报头(此处的报头是struct处理过的)
    header_bytes=phone.recv(header_size)

    #第三步:从报头中解析出对真实数据的描述信息
    header_json=header_bytes.decode('utf-8')
    header_dic=json.loads(header_json)
    print(header_dic)
    total_size=header_dic['total_size']

    #第四步:接收真实的数据
    recv_size=0
    recv_data=b''
    while recv_size < total_size:
        res=phone.recv(1024) 
        recv_data+=res
        recv_size+=len(res)

    print(recv_data.decode('utf-8'))

phone.close()

3.3 基于tcp协议编写简单FTP程序,实现上传、下载文件功能,并解决粘包问题

  • server
import socket
import subprocess
import struct
import json
import os

BASE_DIR = os.path.dirname(__file__)
db_dir=os.path.join(BASE_DIR,'share')

def get(conn,cmds):
    filename = cmds[1]

    # 3、以读的方式打开文件,读取文件内容发送给客户端
    # 第一步:制作固定长度的报头
    header_dic = {
        'filename': filename,  # 'filename':'1.mp4'
        'md5': 'xxdxxx',
        'file_size': os.path.getsize(r'%s/%s' % (db_dir, filename))
    # os.path.getsize(r'/Users/linhaifeng/PycharmProjects/网络编程/05_文件传输/server/share/1.mp4')
    }

    header_json = json.dumps(header_dic)

    header_bytes = header_json.encode('utf-8')

    # 第二步:先发送报头的长度
    conn.send(struct.pack('i', len(header_bytes)))

    # 第三步:再发报头
    conn.send(header_bytes)

    # 第四步:再发送真实的数据
    with open('%s/%s' % (db_dir, filename), 'rb') as f:
        # conn.send(f.read())
        for line in f:
            conn.send(line)

def put(phone,cmds):
    # 2、以写的方式打开一个新文件,接收服务端发来的文件的内容写入客户的新文件
    # 第一步:先收报头的长度
    obj = phone.recv(4)
    header_size = struct.unpack('i', obj)[0]

    # 第二步:再收报头
    header_bytes = phone.recv(header_size)

    # 第三步:从报头中解析出对真实数据的描述信息
    header_json = header_bytes.decode('utf-8')
    header_dic = json.loads(header_json)
    '''
            header_dic={
                'filename': filename, #'filename':'1.mp4'
                'md5':'xxdxxx',
                'file_size': os.path.getsize(filename)
            }
    '''
    print(header_dic)
    total_size = header_dic['file_size']
    filename = header_dic['filename']

    # 第四步:接收真实的数据
    with open('%s/%s' % (db_dir, filename), 'wb') as f:
        recv_size = 0
        while recv_size < total_size:
            line = phone.recv(1024)  # 1024是一个坑
            f.write(line)
            recv_size += len(line)
            print('总大小:%s   已下载大小:%s' % (total_size, recv_size))

def run():
    phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    # phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    phone.bind(('127.0.0.1',8912)) #0-65535:0-1024给操作系统使用
    phone.listen(5)
    print('starting...')
    while True: # 链接循环
        conn,client_addr=phone.accept()
        print(client_addr)
        while True: #通信循环
            try:
                #1、收命令
                res=conn.recv(8096) # b'put 1.mp4'
                if not res:break #适用于linux操作系统
                #2、解析命令,提取相应命令参数
                cmds=res.decode('utf-8').split() #['put','1.mp4']
                if cmds[0] == 'get':
                    get(conn,cmds)
                elif cmds[0] == 'put':
                    put(conn,cmds)
            except ConnectionResetError: #适用于windows操作系统
                break
        conn.close()

    phone.close()


if __name__ == '__main__':
    run()

  • client
import socket
import struct
import json
import os

BASE_DIR = os.path.dirname(__file__)
db_dir=os.path.join(BASE_DIR,'download')
def get(phone,cmds):
    # 2、以写的方式打开一个新文件,接收服务端发来的文件的内容写入客户的新文件
    # 第一步:先收报头的长度
    obj = phone.recv(4)
    header_size = struct.unpack('i', obj)[0]

    # 第二步:再收报头
    header_bytes = phone.recv(header_size)

    # 第三步:从报头中解析出对真实数据的描述信息
    header_json = header_bytes.decode('utf-8')
    header_dic = json.loads(header_json)
    '''
            header_dic={
                'filename': filename, #'filename':'1.mp4'
                'md5':'xxdxxx',
                'file_size': os.path.getsize(filename)
            }
    '''
    print(header_dic)
    total_size = header_dic['file_size']
    filename = header_dic['filename']

    # 第四步:接收真实的数据
    with open('%s/%s' % (db_dir, filename), 'wb') as f:
        recv_size = 0
        while recv_size < total_size:
            line = phone.recv(1024)  # 1024是一个坑
            f.write(line)
            recv_size += len(line)
            print('总大小:%s   已下载大小:%s' % (total_size, recv_size))

def put(conn,cmds):
    filename = cmds[1]

    # 3、以读的方式打开文件,读取文件内容发送给客户端
    # 第一步:制作固定长度的报头
    header_dic = {
        'filename': filename,  # 'filename':'1.mp4'
        'md5': 'xxdxxx',
        'file_size': os.path.getsize(r'%s/%s' % (db_dir, filename))
    # os.path.getsize(r'/Users/linhaifeng/PycharmProjects/网络编程/05_文件传输/server/share/1.mp4')
    }

    header_json = json.dumps(header_dic)

    header_bytes = header_json.encode('utf-8')

    # 第二步:先发送报头的长度
    conn.send(struct.pack('i', len(header_bytes)))

    # 第三步:再发报头
    conn.send(header_bytes)

    # 第四步:再发送真实的数据
    with open('%s/%s' % (db_dir, filename), 'rb') as f:
        # conn.send(f.read())
        for line in f:
            conn.send(line)

def run():
    phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

    phone.connect(('127.0.0.1',8912))

    while True:
        #1、发命令
        inp=input('>>: ').strip() #get a.txt
        if not inp:continue
        phone.send(inp.encode('utf-8'))

        cmds=inp.split() #['get','a.txt']
        if cmds[0] == 'get':
            get(phone,cmds)
        elif cmds[0] == 'put':
            put(phone,cmds)

    phone.close()



if __name__ == '__main__':
    run()

参考:

  1. Python网络编程总结
  2. socket应用实例之RTP音频流传输
  3. 【Socket通信】关于Socket通信原理解析及python实现
  4. Python3.X Socket 一个编码与解码的坑
  5. Winform Socket通信
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值