十三、TCP网络编程

socket套接字

进程之间的交互连接通过网络套接字建立,即双方必须有IP+端口。

python中提供socket标准库,它是非常底层的库,Socket作用在应用层与TCP/IP协议族通信的中间,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部;用户对接口进行调用,由Socket去组织数据,以符合指定的协议。

其实站在编程者的的角度,socket就是一个模块。我们通过调用模块中已经实现的方法建立两个进程之间的连接和通信。
在这里插入图片描述
建立连接的过程
在这里插入图片描述

TCP编程

服务器端编程步骤

  1. 创建Socket对象
  2. 为Socket对象绑定IP地址和端口。使用bind()方法,传入的参数为一个二元组,其格式为(‘IP地址字符串’,Port)
  3. 开始监听,listen()方法 。
  4. 获取用于数据交互的Socket对象,一个进程拥有多个socket对象
    使用accept(),accept方法阻塞等待客户端建立连接,返回一个新的Socket对象和客户端地址的二元组。新Socket对象具备recv(bufsize[,flags]) 使用缓冲区接收数据;send(bytes)发送数据

代码演示上诉步骤

import socket

sk=socket.socket()   ##通过socket模块中的socket的class 实例化一个sokcet,命名为sk
sk.bind(("127.0.0.1",8080))    ##sk绑定在在ip:127.0.0.1 端口8080。注意bind()方法接收的是ip和端口组成的元组
sk.listen()          ##套接字sk开始在指定端口上监听

conn,addr=sk.accept()       ##获得套接字的一个连接对象,和地址对象,此次发送等待阻塞,地址对象也是个元组('127.0.0.1', 55616)
ret=conn.recv(1024)         ##通过连接对象.recv方法 接收内容,1024表示一次接收最多1024字节
print(ret)
conn.send(b"12345")         ##通过连接对象.send方法 传送内容, 传送的内容必须是bytes类型

conn.close()                ##关闭连接对象
sk.close()                  ##销毁套接字sk

案例演示

多人聊天-server

import logging
import socket
import time
import threading

FORMAT = '%(asctime)s %(threadName)s %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)

class ChatSever():
    def __init__(self,ip,port):
        self.sk=socket.socket()
        self.sk.bind((ip,port))
        self.clients=[]

    def start(self):
        self.sk.listen()
        #conn,addr=self.sk.accept() #会发生阻塞等待,阻碍主线程,故线程化需要accept的函数
        threading.Thread(target=self.accept,name='accept').start()

    def stop(self):
        for client in self.clients:
            client.close()
        self.sk.close()

    def accept(self):
        while True:
            conn,addr=self.sk.accept()
            self.clients.append(conn)
            #conn.recv() #会发生阻塞等待,accept的线程卡住,故线程化需要recv的函数
            threading.Thread(target=self.recv,args=(conn,),name='recv').start()

    def recv(self,conn):
        while True:
            data=conn.recv(1024).decode()
            logging.info(data)
            if not data:
                conn.close()
                self.clients.remove(conn)
                break
            for client in self.clients:
                client.send(data.encode())

def showthreadinfo():
    while True:
        time.sleep(1)
        logging.info(threading.enumerate())

cs=ChatSever('127.0.0.1',9999)
cs.start()

while True:
    threading.Thread(target=showthreadinfo,name='jiankong',daemon=True).start()
    cmd=input()
    if cmd=='quit':
        time.sleep(3)
        cs.stop()
        break

多人聊天-client

import logging
import socket
import threading

FORMAT = '%(asctime)s %(threadName)s  %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)

ck=socket.socket()
ck.connect(('127.0.0.1',9999))


def send(ck):
    while True:
        data=input('want to say:')
        ck.send(data.encode())
        logging.info('send {}'.format(data))

def recv(ck):
    while True:
        ret=ck.recv(1024).decode()
        logging.info('recv {}'.format(ret))

threading.Thread(target=send,args=(ck,)).start()
threading.Thread(target=recv,args=(ck,)).start()

黏包问题

在网络传输中,因为接收端是通过缓冲区来接收数据,很容易出现黏包的现象,比如对方连续多次发送,但接收方一次接收,导致接收的数据混在一起。

黏包解决方法:通过自定义报文,告知客户端关于传输内容的属性,如大小、md5码等。

import socket
import struct
import json
import subprocess
sk=socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen()

conn,addr=sk.accept()

while True:
    cmd=conn.recv(1024).decode()
    print(cmd)
    if cmd =='q':break
    ret_cmd=subprocess.Popen(cmd,shell=True,
                             stdout=subprocess.PIPE,stderr=subprocess.PIPE)

    err=ret_cmd.stderr.read()       ##注意此处必须先将ret_cmd的stderr属性read出,然后赋值给某个变量。
                                    ##因为stderr属性类似一个队列,read取走一次后将变空
    if err:
        send_msg=err
    else:
        send_msg=ret_cmd.stdout.read()
    print(send_msg)
    ##判断命令执行结果,然后决定发送给客户端的内容
    data_size=len(send_msg)
    header={"data_size":data_size}      ##定义header报文为一个字典,里面的k-v对用于表示传输内容的大小,也可以扩展说明其他属性
    header_json=json.dumps(header)      ##将header报文字符串化,才能在下面使用struct进行转换。
    header_len_struct=struct.pack('i',len(header_json))     ##将表示header报文的大小的数值,转换成用4个字节大小的bytes数据来表示
    conn.send(header_len_struct)        ##先发送表示header报文大小的bytes类型数据
    conn.send(header_json.encode())     ##再发送header报文
    conn.send(send_msg)                 ##最后发送执行结果

conn.close()
sk.close()

客户端
import socket
import struct
import json

ck=socket.socket()
ck.connect(('127.0.0.1',8080))

while True:
    cmd=input('输入命令:')
    ck.send(cmd.encode())
    if cmd=='q':break
    header_len_struct=ck.recv(4)        ##接收表示报文大小的bytes类型数据
    header_len=struct.unpack('i',header_len_struct)[0]  ##将bytes类型数据反转换,从中取出报文大小。
    header=json.loads(ck.recv(header_len).decode())   ##按header报文的大小接受数据,取得header完整的报文
                                                      ##将报文从字符串反序列化为字典
    print(header)
    cmd_ret=ck.recv(header['data_size']).decode()     ##根据字典中的执行结果大小,获取完整的执行结果
    print(cmd_ret)

ck.close()

socketserver

socket编程过于底层,难以构建健壮的代码,python基于socket模块进行了扩展和完善,推出了socketserver模块,是一种网络服务编程框架,用于构建企业级的应用,已经进入python的标准库。

socketserver模块提供五种服务类

  • BaseServer:服务器的基类,定义了API。
  • TCPServer:使用TCP/IP套接字。
  • UDPServer:使用数据报套接字
  • UnixStreamServer:使用UNIX套接字,只适用UNIX平台
  • UnixDatagramServer:使用UNIX套接字,只适用UNIX平台

这五种服务基类都是同步类型,他们的直接存储继承关系
在这里插入图片描述

在socketserver模块中还提供类Mixin类,与服务类组合,实现server异步。

#创建多进程UDP服务,fork需要操作系统支持,Windows不支持。
class ForkingUDPServer(ForkingMixIn, UDPServer): pass  
#创建多进程TCP服务,fork需要操作系统支持,Windows不支持。
class ForkingTCPServer(ForkingMixIn, TCPServer): pass
#创建多线程UDP服务
class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
#创建多线程TCP服务
class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass

通常编程server端服务时调用上面四种异步类ThreadingUDPServer,ThreadingTCPServer,ForkingUDPServer,ForkingTCPServer

浅分析sockerserver模块中服务类的工作过程,以ThreadingTCPServer类为例

1、通过ThreadingTCPServer的继承,查看BaseServer的__init__方法知道创建服务实例时需要传入server_address, RequestHandlerClass

##sockerserver.py文件中
def __init__(self, server_address, RequestHandlerClass):
    """Constructor.  May be extended, do not override."""
    self.server_address = server_address #服务实例监听的socket,以二元组('ip',port)
    self.RequestHandlerClass = RequestHandlerClass  # 将RequestHandlerClass当作类属性付给了实例本身
    self.__is_shut_down = threading.Event()
    self.__shutdown_request = False

2、服务实例最终调用RequestHandlerClass类来处理一个客户端的连接

def finish_request(self, request, client_address): # 处理请求的方法
"""Finish one request by instantiating RequestHandlerClass.""" 
self.RequestHandlerClass(request, client_address, self) 

3、如何创建RequestHandlerClass呢?在sockerserver模块中提供基类BaseRequestHandler供RequestHandlerClass继承。BaseRequestHandler源码如下:

class BaseRequestHandler:

    # 在初始化时就会调用这些方法
    def __init__(self, request, client_address, server):
        self.request = request
        self.client_address = client_address
        self.server = server
        self.setup()
        try:
            self.handle()
        finally:
            self.finish()

    def setup(self):    # 每一个连接初始化
        pass

    def handle(self):   # 每一次请求处理
        pass 

    def finish(self):   # 每一个连接清理
        pass

由以上源码可以知道在初始化时,setup、handler、finish就会被执行
setup():执行处理请求之前的初始化相关的各种工作。默认不会做任何事。
handler():执行那些所有与处理请求相关的工作。默认也不会做任何事。
finish():执行当处理完请求后的清理工作,默认不会做任何事。

setup、handler、finish方法默认不做任何事,是为了让用户编写一个子类RequestHandlerClass,在子类中编写自己的业务代码,然后传入服务实例中,来处理客户端的请求。

总结一下,使用socketserver创建一个服务器需要以下几个步骤:

  1. 从BaseRequestHandler类派生出子类,并覆盖其handler()方法来创建请求处理程序类,此方法将处理传入请求。
  2. 实例化一个服务器类,传参服务器的地址和请求处理类
  3. 调用服务器实例的handler_request()或serve_forever()方法(启动服务)
  4. 调用server_close()方法(关闭服务)

举例

import socketserver
class MyRequestHandler(socketserver.BaseRequestHandler):
    def handle(self):
        print(self.server)  # <socketserver.TCPServer object at 0x00000153270DA320>
        print(self.client_address)  # ('127.0.0.1', 62124)
        print(self.request)  # <socket.socket fd=296, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 999), raddr=('127.0.0.1', 62124)>

s = socketserver.TCPServer(('127.0.0.1', 999), MyRequestHandler)
s.serve_forever()
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值