python网络编程一--socket开发

socket

socket是非常底层的接口库,它是一种通用的网络编程接口(意思就是java/c++/python都一样),和网络层次没有一一对应关系,而是看你在哪一层进行编程。
cs编程:client-server,bs(browser-server)就是一种cs。只做c开发,就是前端开发,只做s后端开发


1 socket类参数:socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)

  • family
    socket.AF_INET ipv4
    socket.AF_INET6 ipv6
    socket.AF_UNIX

socket.AF_UNIX:

  • 我们都知道socket本来是为网络通讯设计,可以通过socket方便的实现不同机器之间的通信。当然通过socket也可以实现同一台主机之间的进程通信。但是通过socket网络协议实现进程通信效率太低,后来就出现了IPC通信协议,UNIX Domain Socket (UDS)就是其中之一,而且用于IPC更有效率,比如:数据不再需要经过网络协议栈,也不需要进行打包拆包、计算校验和、维护序列号和及实现应答机制等,要做的只是将应用层数据从一个进程拷贝到另一个进程。

  • 类似于Socket的TCP和UDP,UNIX Domain Socket也提供面向流和面向数据包两种实现,但是面向消息的UNIX Domain Socket也是可靠的,消息既不会丢失也不会顺序错乱。UNIX Domain Socket是全双工的,在实现的时候可以明显的发现客户端与服务端通信是通过文件读写进行的通信,而且可以同时读写。相比其它IPC机制有明显的优越性,目前已成为使用最广泛的IPC机制,如果用过supervisor的同学会发现在配置supervisor的时候需要设置一个supervisor.sock的问题地址,不难猜出,supervisor即使用unix socket来进行通信的。

  • 使用UNIX Domain Socket的过程和网络socket十分相似,也要先调用socket()创建一个socket文件描述符,address family指定为AF_UNIX,type可以选择SOCK_DGRAM或SOCK_STREAM,protocol参数仍然指定为0即可。但是我们通过Golang或者其他高级语言实现的时候,会掩盖很多底层信息。

  • UNIX Domain Socket与网络socket编程最明显的不同在于地址格式不同,用结构体sockaddr_un表示,网络编程的socket地址是IP地址加端口号,而UNIX Domain Socket的地址是一个socket类型的文件在文件系统中的路径,这个socket文件由bind()调用创建,如果调用bind()时该文件已存在,则bind()错误返回。所以在实现的时候,可以再启动的时候删掉sock文件,也可以在程序的signal捕获到退出时删除sock文件。

  • type:
    socket.SOCK_STREAM tcp协议
    socket.SOCK_DGRAM UDP协议


2 TCP server端编程步骤

1. 创建一个socket对象(假设叫listen_sock)。socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
2. 绑定ip地址和端口,bind()方法: ip地址与internet层绑定。为什么要绑定端口,端口是应用层协议,一个应用程序对应一个端口,如果不绑定端口,消息发过来后怎么知道是与哪个应用程序通信呢?
3. 打开监听,listen()方法: 光绑定没用啊,不监听服务就起不来。所以第三步是打开监听。监听后就在这个地址的这个端口上开启了一个应用程序服务,占用一个socket。用户根据这个地址端口请求服务,相当于在请求背后的应用程序,这个应用程序进程中的线程在实际处理A端口上的请求。

监听:像门童,等待顾客。顾客到了:你好,顾客请跟我来。并招呼某某服务员(新的Socket对象),请带客户到餐桌,服务员为客户服务-点餐、吃饭

4. 接受用户连接请求,返回与用户建立连接的socket对象,传送数据,listen_sock.accept() 用户发起请求建立一个连接,就占用一个socket资源,而一个socket占用一个文件描述符。所以每一个用户请求的连接需要建立一个新的socket对象。

listen_sock.accept()返回一个socket对象(假设叫new_sock)和客户端IP二元组。用户的请求在这个新的socket对象中处理。这个新的Socket对象就是领顾客到餐桌的服务员。
在新的socket对象new_sock接受数据:new_sock.recv(bufsize[,flag])
在新的socket对象new_sock发送数据:new_sock.send(bytes)
sendfile()方法,python3.5之后引入:在内核态就把数据发送了,减少用户态和内核态的拷贝过程,提高性能,new_sock.sendfile(file,offset=0,count=None)

示例:

import socket
import time
import logging
import datetime

def server_foo():
    listen_sock=socket.socket()  # 1、建立socket对象
    ip='127.0.0.1'
    port=9998
    addr=(ip,port)  # 注意,每个addr只能bind一次
    listen_sock.bind(addr)  # 2、绑定IP和端口
    listen_sock.listen()  # 3、监听端口(至此服务器就启起来了)。注意,监听服务只管监听新的请求,不与请求建立连接
    
    time.sleep(1)
    logging.info('socket:{}'.format(listen_sock))
    
    conn, client_addr = listen_sock.accept()  # 4、接受连接请求,建立连接,传输数据。允许别人连接,有人来请求连接,就派发一个新的socket伙伴conn手拉手玩去。
    # sock.accept()默认阻塞的,所以,执行程序后,这一行日志一直不打印。阻塞着一直等用户请求。当用户发来请求后,返回一个新的socket,用户的请求在新的socket对象处理。sock.accept()继续阻塞监听其他的用户请求。
    
    logging.info('accept:{}'.format(conn))
    logging.info('client address:{}'.format(client_addr))
    data=conn.recv(1024)  # 接收数据。参数`bufsize`--个缓冲区队列,一般取1024的倍数
    logging.info('data:{}'.format(data))
    conn.send('ack }}: {}'.format(data.decode(),datetime.datetime.now()).encode())#发送数据
    
    listen_sock.close()  # 5、释放资源。为什么,因为占用了一个文件描述符,得释放呀
    conn.close()
    # accept和recv都是阻塞的。等IO、等文件描述符、等网络,都是IO请求,IO密集型。线程被阻塞了,这种就不适合放在主线程中,而应该开工作线程。


if __name__ == "__main__":
    # netstat -anp tcp |find "9998"
    server_foo()

3 TCP client端编程步骤

  1. 创建一个socket实例
  2. 发起远程服务端(ip, port)连接请求
  3. 收发数据
  4. 关闭连接、释放资源
import socket
import logging

def client_foo():
    client_sock=socket.socket()  # 1、建立socket对象
    ip='127.0.0.1'
    port=9998
    addr=(ip,port)  # 注意,每个addr只能bind一次
    client_sock.connect(addr)  # 2、连接服务器
    data = client_sock.recv(1024)  # 3、接收数据
    logging.info('accept:{}'.format(data.decode()))
    client_sock.send(data)  # 发送数据
    client_sock.close()  # 释放资源

3 socket对象其他常用方法

属性 含义
socket.makefile() 把一个socket对象包装成一个文件对象fileObj,然后把这个socket对象的数据收、发过程当文件处理
sock.getpeername() 获取socket远端的地址(address, port)
sock.getsockname() 获取socket的本段自己的地址(address, port)
socket.recvfrom() 返回一个二元组,数据和远端的地址。
socket.sendfile(file,offset=0,count=None) 在内核态就把数据发送了,减少用户态和内核态的拷贝过程,提高性能

4 server端应用

写一个群聊工具server端。 要求:

  1. 启动服务:绑定地址、打开监听
  2. 建立连接:可以和多个用户建立连接
  3. 接受不同用户的信息
  4. 分发,将某个用户信息转发到所有连接的客户端
  5. 停止服务
  6. 记录连接的客户端

代码实现

import socket
import threading
import time

from tool.logger_define import LoggerDefine


logger = LoggerDefine(__file__).get_logger


class ChatServer:
    def __init__(self, ip='127.0.0.1', port=9998):
        self.addr = (ip, port)
        self.clients = dict()
        self.listen_sock = socket.socket()
        self.event = threading.Event()
        self.worker_num = 1

    def start(self):
        logger.info('Start server')
        self.listen_sock.bind(self.addr)
        self.listen_sock.listen()
        threading.Thread(target=self._accept).start()

    def _accept(self):
        while not self.event.is_set():
            try:
                conn, client_addr = self.listen_sock.accept()
            except OSError as e:
                logger.error("connection closed. error: {}".format(e))
                break
            self.clients[client_addr] = conn
            logger.info('Start e worker thread:worker_{} to handler request:{}'.format(self.worker_num, client_addr))
            threading.Thread(
                target=self._recv, args=(conn, client_addr), name='worker_{}'.format(self.worker_num)).start()
            self.worker_num += 1

    def _recv(self, conn: socket.socket, client_addr):
        while not self.event.is_set():
            try:
                data = conn.recv(1024)
            except ConnectionAbortedError as e:
                logger.error('break connection:{}, error:{}'.format(client_addr[0], e))
                self.clients.pop(client_addr)
                break
            trans_data = data.decode()
            logger.info("{} Recv Data:{}".format(threading.current_thread().name, trans_data))
            if trans_data.strip() == 'quit':
                self.clients.pop(client_addr)
                logger.info('close connection:{}'.format(client_addr[0]))
                conn.close()
                break
            new_data = "{} say:{}".format(client_addr[0], trans_data)
            logger.info("{} Ack:{}".format(threading.current_thread().name, new_data))
            for tmp_key in list(self.clients.keys()):
                self._send(tmp_key, new_data)

    def _send(self, addr, data):
        conn = self.clients.get(addr)
        try:
            conn.send(data.encode())
        except ConnectionError as e:
            logger.error("Error:{}, ip:{}, delete it".format(e, addr[0]))
            self.clients.pop(addr)

    def stop(self):
        logger.info('Stop connection client.')
        for addr in list(self.clients.keys()):
            conn = self.clients.get(addr)
            try:
                conn.close()
            except ConnectionAbortedError as e:
                logger.error('Error:{}, pass'.format(e))
        self.event.wait(3)
        self.event.set()
        logger.info('stop listen socket.')
        self.listen_sock.close()


def show_thread(_event: threading.Event):
    while not _event.is_set():
        logger.info("Current thread:{}".format(threading.enumerate()))
        time.sleep(6)


if __name__ == "__main__":
    # netstat -anp tcp |find "9998"
    my_chat = ChatServer()
    my_chat.start()
    event = threading.Event()
    threading.Thread(target=show_thread, args=(event,), daemon=True).start()
    while True:
        cmd = input(">>>").strip()
        if cmd == "quit":
            my_chat.stop()
            break
    time.sleep(2)
    logger.info('exist server')

5 TCP client端实现

实现TCP client端:与服务器建立连接、发生数据、接收数据、主动断开连接

代码实现

import socket
import threading

from tool.logger_define import LoggerDefine


logger = LoggerDefine(__file__).get_logger


class MyTCPClient:
    def __init__(self, addr='127.0.0.1', port=9998):
        self.addr = (addr, port)
        self.sock = socket.socket()
        self.ev = threading.Event()

    def start(self):
        logger.info("start client")
        self.sock.connect(self.addr)
        threading.Thread(target=self.recv_data, name="recv_data").start()

    def recv_data(self):
        while not self.ev.is_set():
            try:
                data = self.sock.recv(1024)
            except ConnectionError as e:
                logger.error("ERROR:{}".format(e))
                self.sock.close()
                break
            logger.info("Recv:{}".format(data.decode()))

    def send_data(self, data: bytes = b"quit"):
        try:
            self.sock.send(data)
        except OSError as e:
            self.stop()
            logger.error("ERROR:{}".format(e))
            raise e

    def stop(self):
        self.sock.close()
        self.ev.wait(3)
        self.ev.set()


if __name__ == '__main__':
    my_client = MyTCPClient()
    my_client.start()
    while True:
        cmd = input(">>>").strip()
        try:
            my_client.send_data(cmd.encode())
        except OSError as e:
            my_client.stop()
            logger.error("ERROR:{}".format(e))
            break
        if cmd == "quit":
            my_client.stop()
            break
    logger.info("Stop client")

6 UDP server端和client端流程

UDP是无连接协议,它基于一个假设:

  • 网络足够好
  • 消息不会丢包
  • 包不会乱序

但是,即使是在局域网,也不能保证不丢包,而且包的到达不一定高序.

应用场景

  • 视频、音频传输,一般来说,丢些包,问题不大,最多丢些图像、 听不清说话,可以重新说话来 解决.
  • 海量采集数据,例如传感器发来的数据,丢几十、几百条数据也没有关系.
  • DNS协议,数据内容小,一个包就能查询到结果,不存在乱序,丢包,重新请求解析.

一般来说, UDP性能优于TCP,但是可靠性要求高的场合的还是要选择TCP协议.

server端

  1. 创建一个socket实例
  2. 绑定地址和端口:socket.bind()
  3. 接收和应答数据:socket.recvfrom(1024), socket.sendto(addr)
  4. 释放资源:socket.close()

client端

  1. 创建一个socket实例
  2. 注册、发送数据
  3. 接收应答数据
  4. 释放资源
import socket
import logging

def server_foo():
    client_sock=socket.socket()  # 1、建立socket对象
    ip='127.0.0.1'
    port=9998
    addr=(ip,port)  # 注意,每个addr只能bind一次
    client_sock.connect(addr)  # 2、连接服务器
    data = client_sock.recv(1024)  # 3、接收数据
    logging.info('accept:{}'.format(data.decode()))
    client_sock.send(data)  # 发送数据
    client_sock.close()  # 释放资源

def client_foo():
    client_sock=socket.socket()  # 1、建立socket对象
    ip='127.0.0.1'
    port=9998
    addr=(ip,port)  # 注意,每个addr只能bind一次
    client_sock.connect(addr)  # 2、连接服务器
    data = client_sock.recv(1024)  # 3、接收数据
    logging.info('accept:{}'.format(data.decode()))
    client_sock.send(data)  # 发送数据
    client_sock.close()  # 释放资源

7 UDP server实现群聊

  1. 启动服务:绑定地址、打开监听
  2. 建立连接:可以和多个用户建立连接
  3. 接受不同用户的信息
  4. 分发,将某个用户信息转发到所有连接的客户端
  5. 停止服务
  6. 记录连接的客户端

代码实现

import datetime
import socket
import threading

from tool.logger_define import LoggerDefine

logger = LoggerDefine(__file__).get_logger


class MyUDPChatServer:
    def __init__(self, addr='127.0.0.1', port=9998):
        self.addr = (addr, port)
        self.sock = socket.socket(type=socket.SOCK_DGRAM)
        self.ev = threading.Event()
        self.clients = dict()
        self.interval = 10

    def start(self):
        logger.info("start server")
        self.sock.bind(self.addr)
        threading.Thread(target=self.recv_data, name='recv_data').start()

    def recv_data(self):
        while not self.ev.is_set():
            try:
                data, client_addr = self.sock.recvfrom(1024)
            except OSError as e:
                logger.error("ERROR:{}".format(e))
                continue
            reg_time = datetime.datetime.now().timestamp()
            msg = data.decode()
            if msg == "quit":
                if self.clients.get(client_addr):
                    self.clients.pop(client_addr)
                continue
            elif msg == "^o^" or msg == "reg":
                logger.info("first reg, pass")
                self.clients[client_addr] = reg_time
                continue
            if not self.clients.get(client_addr):
                self.clients[client_addr] = reg_time
            self.send_data(msg)

    def send_data(self, data: str = "quit"):
        cur_time = datetime.datetime.now().timestamp()
        ack_msg = "ack:{}".format(data)
        logger.info(ack_msg)
        for client_addr in list(self.clients.keys()):
            reg_time = self.clients.get(client_addr)
            if cur_time - reg_time > 10:
                self.clients.pop(client_addr)
                continue
            try:
                self.sock.sendto(ack_msg.encode(), client_addr)
            except OSError as e:
                logger.error("error:{}".format(e))
                self.clients.pop(client_addr)

    def stop(self):
        self.send_data()
        self.ev.set()
        self.sock.close()
        self.ev.wait(3)


def show_thread():
    ev = threading.Event()
    while not ev.wait(6):
        logger.info(threading.enumerate())


if __name__ == '__main__':
    my_server = MyUDPChatServer()
    my_server.start()
    threading.Thread(target=show_thread, name="show_thread", daemon=True).start()
    while True:
        cmd = input(">>>").strip()
        if cmd == "quit":
            my_server.stop()
            break
    logger.info('Stop server')

8 UDP client实现

实现TCP client端:与服务器建立连接、发生数据、接收数据、主动断开连接

ack机制和心跳heartbeat.
心跳,就是一端定时发往另一端的信息,一般每次数据越少越好。心跳时间间隔约定好就行。ack即响应,一端收到另一端的消息后返回的信息。

  • 心跳信息:一般来说是客户端定时发往服务端的,服务端并不需要ack回复客户端,只需要记录该客户端还活着就行了。
  • ack:如果是服务端定时发往客户端的,一般需要客户端ack响应来表示活着,如果没有收到ack的客户端,服务端移除其信息.这种实现较为复杂,用的较少。也有双向都发心跳的,用的更少。
import socket
import threading

from tool.logger_define import LoggerDefine


logger = LoggerDefine(__file__).get_logger


class MyUDPClient:
    def __init__(self, addr='127.0.0.1', port=9998, interval=3):
        self.addr = (addr, port)
        self.sock = socket.socket(type=socket.SOCK_DGRAM)
        self.ev = threading.Event()
        self.interval = interval

    def start(self):
        self.sock.connect(self.addr)
        # self.sock.sendto("reg".encode(), self.addr)  # 使用sendto替换connect,目的都是初始注册。使用sendto时,sever端要增加判断,不用ack注册信息,使用sendto时,
        threading.Thread(target=self.recv_data, name='recv_data').start()
        threading.Thread(target=self.send_heartbeat, name='heartbeat', daemon=True).start()  # 心跳线程最适合用daemon了

    def recv_data(self):
        while not self.ev.is_set():
            try:
                data = self.sock.recv(1024)
            except ConnectionError as e:
                logger.error("Error:{}".format(e))
                self.stop()
                raise e
            except Exception as e:
                logger.error("Error:{}".format(e))
                self.stop()
                raise e
            logger.info("recv:{}".format(data.decode()))

    def send_heartbeat(self):
        while not self.ev.wait(self.interval):
            try:
                self.sock.send("^o^".encode())  # 心跳包不宜过长,且不能是常用发送信息
            except Exception as e:
                logger.error("Error:{}".format(e))
                self.stop()
                break

    def send_data(self, data: str = "quit"):
        try:
            self.sock.send(data.encode())
        except OSError as e:
            logger.error("Error:{}".format(e))
            self.stop()
            raise e
        except Exception as e:
            logger.error("Error:{}".format(e))
            self.stop()
            raise e

    def stop(self):
        self.ev.set()
        self.sock.close()
        logger.info("stop client")


if __name__ == '__main__':
    logger.info("start")
    my_client = MyUDPClient()
    my_client.start()
    while True:
        cmd = input(">>>").strip()
        if cmd == "quit":
            my_client.send_data()
            my_client.stop()
            break
        my_client.send_data(cmd)
    logger.info('end')


9 socketserver模块

socketserver模块,是对底层接口socket的封装。帮助用户写出健壮的代码。用户不用关心怎么去实现socket编程,而是专注于利用socket编程。

  • 用户只需继承BaseRequestHandler后,重写handle方法,去实现自己要如何收、发数据。
  • socketserver模块提供的不同的类,但是编程接口是一样的,即使是多进程、多线程的类也是一样,大大减少了编程的难度。

socketserver简化了网络服务器的编写。
BaseServer下,有四个子类:TCPServer、UDPServer、UnixStreamServer、UnixDatagramServer

通过两个Mixin类(ThreadingMixIn和ForkingMixIn)来支持异步:ForkingTCPServer(ForkingMixIn,TCPServer)…

编程接口socketserver.BaseServer(server_address,RequestHandlerClass):用户提供服务器需要绑定的地址和端口,以及用于处理请求的RequestHandlerClass类,即可。

  • BaseServer中处理请求的方法:BaseServer.finish_request(),这个方法会实例化处理请求方法。
  • BaseServer.serve_forever方法:建立长连接,忽略超时。而handle_request方法有超时现在
  • BaseServer.shutdown: stops the serve_forever
  • BaseServer.server_close:关闭socket

RequestHandlerClass:处理用户请求的处理类。 BaseServer实例接收用户请求后,最后会实例化这个类。

  • RequestHandlerClass类的handle方法:这个类被初始化时,传入3个参数:与客户端连接的socket实例、客户端地址、BaseServer实例。所以在handle方法中,利用socket实例收、发数据。其实handle方法,与socket的accept、recv、send对应。
  • RequestHandlerClass类的setup方法:每一个连接初始化,可以在setup中进行一些初始化操作
  • RequestHandlerClass类的finish方法:每一个连接清理。

创建server一般步骤

  1. 继承BaseRequestHandler实现自己的handler类,覆盖父类的handle方法
  2. 实例化一个server类(TCPServer、ThreadingTCPServer等),传入服务器地址和端口,以及自己的handle类
  3. 处理请求:调用server实例的server_forever或handle_request方法
  4. 释放资源:调用server实例的shutdown方法等待server_forever结束;server_close释放socket。

使用server_forever,时,释放资源先调用shutdown停止server_forever,再调用server_close。而使用handler_request时,就不用调用shutdown,直接server_close

10 使用socketserver模块实现群聊

import socket
import socketserver
import threading
from socketserver import BaseRequestHandler, BaseServer
from tool.logger_define import get_log
from tool.show_thread import show_thread


logger = get_log(__name__)  # 自定义日志模块


class MyHandler(BaseRequestHandler):
    clients = dict()

    def __init__(self, request: socket.socket, client_address: tuple, server: BaseServer) -> None:
        self.ev = threading.Event()
        super().__init__(request, client_address, server)

    def setup(self) -> None:
        self.clients[self.client_address] = self.request
        super().setup()

    def handle(self) -> None:
        while not self.ev.is_set():
            try:
                data = self.request.recv(1024).decode().strip()
            except Exception as e:
                logger.error("error:{}".format(e))
                break
            if data == "quit":
                logger.info("exit")
                break
            msg = "ack:{}".format(data).encode()
            logger.info("recv:{}".format(data))
            for client in list(self.clients.keys()):
                conn = self.clients.get(client)
                try:
                    conn.send(msg)
                except Exception as e:
                    logger.error("Error:{}".format(e))
                    self.clients.pop(client)

    def finish(self) -> None:
        self.ev.set()
        self.clients.pop(self.client_address)
        super().finish()


def my_server(addr='127.0.0.1', port=9998):
    address = (addr, port)
    _tcp_server = socketserver.ThreadingTCPServer(address, MyHandler)
    threading.Thread(target=_tcp_server.serve_forever, name="myserver").start()
    return _tcp_server


if __name__ == '__main__':
    logger.info("start server")
    tcp_server = my_server()
    show_thread()  # 自定义查看线程工具
    while True:
        cmd = input(">>>").strip()
        if cmd == "quit":
            tcp_server.shutdown()
            tcp_server.server_close()
            break
    logger.info("end")

总结:

  • ThreadingTCPServer 为每一个连接提供RequestHandleClass类的实例, 一次调用setup、handle、finish方法,且使用了try... finally结构保证finish方法一定被调用;
  • 这些方法一次执行完成,如如果想维持这个连接和客户端通信,就需要在handle函数中使用循环

你可能感兴趣的:(python网络开发,python,websocket)