【Python】socket_实现本地服务器及客户端交互

发布时间 2023-04-27 17:00:48作者: Phoenixy

一、Http Server : 命令行 http服务器

cmd 窗口命令

python -m http.server 端口号 # Python3 默认8000,可指定(如8080)

python -m SimpleHTTPServer 端口号 #  Python2 默认8000,可指定(如8080)

 

执行后,可通过浏览器里输入http://localhost:8080 <localhost可替换为本机IP> 访问服务器下的资源

 

二、socket 套接字,一个双方通信的接口

1、通信流程图

 

2、socket 函数

 

sk.bind(address)

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

sk.listen(backlog)

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

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

sk.setblocking(bool)

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

sk.accept()

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

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

sk.connect(address)

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

sk.connect_ex(address)

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

sk.close()

  关闭套接字

sk.recv(bufsize[,flag])

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

sk.recvfrom(bufsize[.flag])

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

sk.send(string[,flag])

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

sk.sendall(string[,flag])

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

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

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

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

sk.settimeout(timeout)

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

sk.getpeername()

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

sk.getsockname()

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

sk.fileno()

  套接字的文件描述符
scoket更多功能

 

3、服务端代码

# -*- coding: UTF-8 -*-
import json
import sys
import socket
from loguru import logger as logs


class server_tools:
    """HTTP 服务器都是基于TCP的socket连接"""

    def __init__(self, port: int = 8080, ip_mode="local"):
        """
        构造函数

        :param port: 服务的端口号; 默认 8080 ,可自定义端口
        :param ip_mode: 服务器访问方式; 默认仅本机可以访问, 可选:[lan 局域网, local 本机]
        """

        self.port = port

        # ip访问: <局域网内的ip地址都可以访问>  本机访问: <127.0.0.1 和localhost都可以访问> <0.0.0.0 表示所有的可用的地址>
        self.ip = socket.gethostbyname(socket.gethostname()) if ip_mode.lower() == 'lan' else '127.0.0.1'
        # logs.debug(self.ip)

        try:
            # 第一步:创建socket对象 <family参数:AF_INET家族包括Internet地址(即服务器之间网络通信), AF_UNIX家族用于同一台机器上的进程间通信。 type参数:SOCK_STREAM(流套接字 for TCP), SOCK_DGRAM(数据报套接字 for UDP) >
            self.server_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)

            # 第二步:连接客户端 将socket绑定到指定地址 <AF_INET所创建的套接字,address地址必须是一个双元素元组 (host, port)>
            self.server_socket.bind((self.ip, self.port))

            # 第三步:接收连接请求<不接受关键字参数 backlog指定最多允许多少个客户连接到服务器。它的值至少为1。收到连接请求后,这些请求需要排队,如果队列满,就拒绝请求>
            self.server_socket.listen(128)
        except Exception as e:
            logs.error("Server operation failed, reason for failure: {}".format(e))
            sys.exit()
        else:
            logs.info('{}:{} server is running: '.format(self.ip, self.port))

    def __del__(self):
        # 最后一步:关闭连接
        self.server_socket.close()

    def conn_server(self):
        """
        HTTP 服务器都是基于TCP的socket连接

        :return:
        """

        # 第四步:等待客户请求一个连接 <返回两个元素的元组(connection,address), connection:客户端的socket连接, address:客户端的ip和端口号>
        client_socket, client_addr = self.server_socket.accept()
        # 设置超时时间(单位:s)
        client_socket.settimeout(5)

        # 服务器和客户端通过send和recv方法通信(传输 数据)
        # 第五步:recv 接收客户信息 <从客户端的socket中获取数据>
        rev_data = client_socket.recv(1024)
        logs.info('接收到了来自{}地址{}端口号的的信息,内容是:{}'.format(client_addr[0], client_addr[1], rev_data.decode('utf-8')))

        # 第六步:给客户端的返回消息 <send 采用字符串形式 返回已发送的字符个数> <在返回内容之前需要设置HTTP响应头, 设置一个响应头就换一行, 所有的响应头设置完成后再换行>
        # client_socket.send('HTTP/1.1 200 OK\n'.encode('utf-8'))
        # client_socket.send('content-type:text/html\n'.encode('utf-8'))
        # client_socket.send('\n'.encode('utf-8'))
        # client_socket.send(client_addr[0].encode('utf-8'))

        # 给客户端的返回消息
        send_data = 'HTTP/1.1 200 OK\n content-type:text/html\n\n {}'.format({"status": "10000", "msg": "成功"})
        client_socket.send(send_data.encode("utf-8"))

    def conn_server_revnum(self, order_num=1):
        """
        访问有限次数后关闭服务

        :param order_num:  网站访问次数, True 时
        :return:
        """
        order_num = order_num if isinstance(order_num, (int, bool)) else sys.exit("参数 order_num 不符合要求")

        # 重复四五六步骤:接收请求次数,达到次数后关闭连接
        while order_num:
            self.conn_server()
            order_num -= 1

    def conn_server_loop(self):
        """
        可以无线访问服务,需手动关闭

        :return:
        """

        # 重复四五六步骤:接收请求次数,达到次数后关闭连接
        while True:
            self.conn_server()


if __name__ == "__main__":
    """run"""
    # server_tools().conn_server()  # 接收单次
    # server_tools().conn_server_revnum(3)    # 接收多次
    server_tools().conn_server_loop()    # 无限接收,手动关闭

 

 执行结果 

 开启服务:

# -*- coding:UTF-8 -*-
import json
import requests
from loguru import logger as logs


if __name__ == "__main__":
    """run"""
    url = "http://localhost:8080/"
    header = {'Connection': 'Keep-Alive', 'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Basic Test'}
    data = {"name": "zhangsan", "age": "21", "addr": "北京1"}
    resp = requests.post(url=url, headers=header, data=json.dumps(data, ensure_ascii=False).encode("utf-8"))
    logs.debug(resp)
    logs.debug(resp.text)
    logs.debug(type(resp.text))
向服务器发送请求

 

 执行结果

 服务端接收日志

 

 3.1、根据请求路径返回不同信息

    def conn_server(self):
        """
        HTTP 服务器都是基于TCP的socket连接

        :return:
        """

        # 第四步:等待客户请求一个连接 <返回两个元素的元组(connection,address), connection:客户端的socket连接, address:客户端的ip和端口号>
        client_socket, client_addr = self.server_socket.accept()
        # 设置等待客户请求连接的超时时间(单位:s)<即时间范围内无响应则自动断开服务>
        # client_socket.settimeout(30)

        # 服务器和客户端通过send和recv方法通信(传输 数据)
        # 第五步:recv 接收客户信息 <从客户端的socket中获取数据>
        rev_data = client_socket.recv(1024)
        logs.info('接收到了来自{}地址{}端口号的的信息,内容是:{}'.format(client_addr[0], client_addr[1], rev_data.decode('utf-8')))

        # 第六步:给客户端的返回消息 <send 采用字符串形式 返回已发送的字符个数> <在返回内容之前需要设置HTTP响应头, 设置一个响应头就换一行, 所有的响应头设置完成后再换行>
        # client_socket.send('HTTP/1.1 200 OK\n'.encode('utf-8'))
        # client_socket.send('content-type:text/html\n'.encode('utf-8'))
        # client_socket.send('\n'.encode('utf-8'))
        # client_socket.send(client_addr[0].encode('utf-8'))

        # 给客户端返回的请求头
        send_header = 'HTTP/1.1 200 OK\n content-type:text/html\n\n'
        error_send_header = 'HTTP/1.1 404 Page Not Found\n content-type:text/html \n\n'

        # 根据请求路径判断给出返回数据
        try:
            path = rev_data.splitlines()[0].split()[1].decode('utf-8')  # 切字符串
            logs.debug('请求的路径是:{}'.format(path))
        except Exception as e:
            logs.warning("获取请求路径时出现异常:{}".format(e))
            path = '/'

        logs.debug(path.split('/')[-1])
        if path.startswith('/html/'):
            # send_data = 'HTTP/1.1 200 OK\n content-type:text/html \n\n {}'.format("欢迎来到本地页面!~").encode("gbk")
            # with open("../data/html/login.html", "rb") as f:
            #     send_data = f.read()("gbk")
            try:
                with open("../data/html/{}.html".format(path.split('/')[-1]), "r+", encoding='utf-8') as f:
                    send_data = f.read()
            except FileNotFoundError as fne:
                send_header = error_send_header
                send_data = "页面资源缺失, {}".format(fne)
        elif path == '/postapi':
            send_data = {"status": "10000", "msg": "成功"}
            send_data = str(send_data)
        elif path == '/client':
            send_data = "Hi, client~ server有接收到你的消息哦~"
            send_data = str(send_data)
        else:
            send_header = error_send_header
            send_data = "您访问的资源不存在哦!~".format(path)

        # 给客户端的返回消息
        try:
            client_socket.send('{}{}'.format(send_header, send_data).encode("utf-8"))
            if not self.loop:
                client_socket.close()
        except Exception as e:
            logs.error("给客户端的返回消息时发生错误:{}".format(e))

 

# -*- coding: UTF-8 -*-
import sys
import socket
from loguru import logger as logs


class server_tools:
    """HTTP 服务器都是基于TCP的socket连接"""

    def __init__(self, host=None, port: int = 8080):
        """
        构造函数

        :param port: 服务的端口号; 默认 8080 ,可自定义端口
        :param ip: 服务器ip地址
        """

        self.port = port
        self.loop = False

        # ip访问: <局域网内的ip地址都可以访问>  本机访问: <127.0.0.1 和localhost都可以访问> <0.0.0.0 表示所有的可用的地址>
        self.ip = '0.0.0.0' if host is None else host
        # logs.debug(self.ip)

        try:
            # 第一步:创建socket对象 <family参数:AF_INET家族包括Internet地址(即服务器之间网络通信), AF_UNIX家族用于同一台机器上的进程间通信。 type参数:SOCK_STREAM(流套接字 for TCP), SOCK_DGRAM(数据报套接字 for UDP) >
            self.server_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)

            # 第二步:连接客户端 将socket绑定到指定地址 <AF_INET所创建的套接字,address地址必须是一个双元素元组 (host, port)>
            self.server_socket.bind((self.ip, self.port))

            # 第三步:接收连接请求<不接受关键字参数 backlog指定最多允许多少个客户连接到服务器。它的值至少为1。收到连接请求后,这些请求需要排队,如果队列满,就拒绝请求>
            self.server_socket.listen(128)
        except Exception as e:
            logs.error("Server operation failed, reason for failure: {}".format(e))
            sys.exit()
        else:
            logs.info('{}:{} server is running: '.format(self.ip, self.port))

    def __del__(self):
        # 最后一步:关闭连接
        if not self.loop:
            self.server_socket.close()

    def conn_server(self):
        """
        HTTP 服务器都是基于TCP的socket连接

        :return:
        """

        # 第四步:等待客户请求一个连接 <返回两个元素的元组(connection,address), connection:客户端的socket连接, address:客户端的ip和端口号>
        client_socket, client_addr = self.server_socket.accept()
        # 设置等待客户请求连接的超时时间(单位:s)<即时间范围内无响应则自动断开服务>
        # client_socket.settimeout(30)

        # 服务器和客户端通过send和recv方法通信(传输 数据)
        # 第五步:recv 接收客户信息 <从客户端的socket中获取数据>
        rev_data = client_socket.recv(1024)
        logs.info('接收到了来自{}地址{}端口号的的信息,内容是:{}'.format(client_addr[0], client_addr[1], rev_data.decode('utf-8')))

        # 第六步:给客户端的返回消息 <send 采用字符串形式 返回已发送的字符个数> <在返回内容之前需要设置HTTP响应头, 设置一个响应头就换一行, 所有的响应头设置完成后再换行>
        # client_socket.send('HTTP/1.1 200 OK\n'.encode('utf-8'))
        # client_socket.send('content-type:text/html\n'.encode('utf-8'))
        # client_socket.send('\n'.encode('utf-8'))
        # client_socket.send(client_addr[0].encode('utf-8'))

        # 给客户端返回的请求头
        send_header = 'HTTP/1.1 200 OK\n content-type:text/html\n\n'
        error_send_header = 'HTTP/1.1 404 Page Not Found\n content-type:text/html \n\n'

        # 根据请求路径判断给出返回数据
        try:
            path = rev_data.splitlines()[0].split()[1].decode('utf-8')  # 切字符串
            logs.debug('请求的路径是:{}'.format(path))
        except Exception as e:
            logs.warning("获取请求路径时出现异常:{}".format(e))
            path = '/'

        logs.debug(path.split('/')[-1])
        if path.startswith('/html/'):
            # send_data = 'HTTP/1.1 200 OK\n content-type:text/html \n\n {}'.format("欢迎来到本地页面!~").encode("gbk")
            # with open("../data/html/login.html", "rb") as f:
            #     send_data = f.read()("gbk")
            try:
                with open("../data/html/{}.html".format(path.split('/')[-1]), "r+", encoding='utf-8') as f:
                    send_data = f.read()
            except FileNotFoundError as fne:
                send_header = error_send_header
                send_data = "页面资源缺失, {}".format(fne)
        elif path == '/postapi':
            send_data = {"status": "10000", "msg": "成功"}
            send_data = str(send_data)
        elif path == '/client':
            send_data = "Hi, client~ server有接收到你的消息哦~"
            send_data = str(send_data)
        else:
            send_header = error_send_header
            send_data = "您访问的资源不存在哦!~".format(path)

        # 给客户端的返回消息
        try:
            client_socket.send('{}{}'.format(send_header, send_data).encode("utf-8"))
            if not self.loop:
                client_socket.close()
        except Exception as e:
            logs.error("给客户端的返回消息时发生错误:{}".format(e))

    def conn_server_revnum(self, order_num=1):
        """
        访问有限次数后关闭服务

        :param order_num:  网站访问次数, True 时
        :return:
        """
        order_num = order_num if isinstance(order_num, (int, bool)) else sys.exit("参数 order_num 不符合要求")

        # 重复四五六步骤:接收请求次数,达到次数后关闭连接
        while order_num:
            self.loop = True
            self.conn_server()
            order_num -= 1
        self.loop = False

    def conn_server_loop(self):
        """
        可以无线访问服务,需手动关闭

        :return:
        """
        # 重复四五六步骤:接收请求次数,达到次数后关闭连接
        while True:
            self.loop = True
            self.conn_server()


if __name__ == "__main__":
    """run"""
    # server_tools().conn_server()  # 接收单次
    # server_tools().conn_server_revnum(3)    # 接收多次
    server_tools().conn_server_loop()    # 无限接收,手动关闭
完整实现

 

 

# -*- coding:UTF-8 -*-
import json
import requests
from loguru import logger as logs


if __name__ == "__main__":
    """run"""
    # url = "http://localhost:8080/login"
    url = "http://localhost:8080/postapi"
    header = {'Connection': 'Keep-Alive', 'Content-Type': 'application/json;charset=UTF-8', 'Authorization': 'Basic Test'}
    data = {"name": "zhangsan", "age": "21", "addr": "北京1"}
    # resp = requests.get(url=url, headers=header, data=json.dumps(data, ensure_ascii=False).encode("utf-8"))
    resp = requests.post(url=url, headers=header, data=json.dumps(data, ensure_ascii=False).encode("utf-8"))
    logs.debug(resp)
    logs.debug(resp.text)
接口请求
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>login页面</title>
    <style type="text/css">
        body {
            background-color: aquamarine;
            background-image: url("https://images.cnblogs.com/cnblogs_com/blogs/701705/galleries/2142702/o_221101073829_2da05129f51279e4812decbbb9da1b44.jpeg");
            /*background-image: url("https://images.cnblogs.com/cnblogs_com/blogs/701705/galleries/2142702/o_221101073506_%E7%8B%90%E7%8B%B8.jpeg");*/
            background-repeat: no-repeat;
            background-attachment: fixed;
            background-size: 1800px;
        }
        .div-addr {
            margin-top: 50px;
            margin-bottom: 200px;
            margin-left: 100px;
        }
    </style>
</head>
<body>
    <div class="div-addr">
        <h2>Hello, welcome to login ~~~~ </h2>
    </div>
</body>
</html>
login.html

 

执行结果

 接口返回: 

 

 4、客户端代码

 

# -*- coding: UTF-8 -*-
import sys
import socket
from loguru import logger as logs


class client_tools:
    """HTTP 服务器都是基于TCP的socket连接"""

    def __init__(self, host: str = None, port: int = 8080):
        """
        构造函数

        :param host: 需要连接服务的主机名/IP; 默认 本机ip ,可自定义
        :param port: 需要连接服务的端口号; 默认 8080 ,可自定义
        """

        # ip访问<局域网内的ip地址都可以访问: 127.0.0.1 和localhost都可以访问 <127.0.0.1 和0.0.0.0都表示本机, 0.0.0.0表示所有的可用的地址>>
        self.ip = socket.gethostbyname(socket.gethostname()) if host is None else host
        self.port = port

        try:
            # 创建socket对象 <family参数:AF_INET家族包括Internet地址(即服务器之间网络通信), AF_UNIX家族用于同一台机器上的进程间通信。 type参数:SOCK_STREAM(流套接字 for TCP), SOCK_DGRAM(数据报套接字 for UDP) >
            self.client_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
            # 连接服务端
            # # 连接到address处的套接字 [.connect: <出错时返回socket.error错误> .connect_ex: <区别:连接成功时返回 0 ,连接失败时候返回编码>]
            self.client_socket.connect((self.ip, self.port))
            # conn_code = self.client_socket.connect_ex((self.ip, self.port))
        except Exception as e:
            logs.error("Client operation failed, reason for failure: {}".format(e))
            sys.exit()
        else:
            logs.info('{}: {} client is running: '.format(self.ip, self.port))

    def __del__(self):
        # 关闭连接
        self.client_socket.close()

    def conn_client(self, msg='I am client'):
        """
        TCP 客户端与 TCP server通信

        :param msg:发送的信息
        :return:
        """
        url = 'POST /client \n'
        header = 'HTTP/1.1 200 OK\ncontent-type:text/html\n\n'
        msg = input("Please input msg: ") if msg == '' else msg
        # 服务器和客户端通过send和recv方法通信(传输 数据)
        # 向服务端发送请求
        self.client_socket.sendall("{}{}{}".format(url, header, msg).encode("utf-8"))
        # self.client_socket.send(url.encode('utf-8'))
        # self.client_socket.send(header.encode('utf-8'))
        # self.client_socket.send(msg.encode("utf-8"))

        # recv 接收客户信息 <从客户端的socket中获取数据>
        data = self.client_socket.recv(1024)
        logs.info('接收服务端返回的信息,内容是:{}'.format(data.decode('utf-8')))


if __name__ == "__main__":
    """run"""
    msg = ''
    # client_tools().conn_client()
    client_tools(host='127.0.0.1', port=8080).conn_client()

 

 

执行结果:

 

 

 

 

 

 

 

 

官方地址:https://docs.python.org/zh-cn/3/library/socket.html

参考地址:

  https://www.cnblogs.com/csnd/p/16465082.html

  https://www.cnblogs.com/aylin/p/5572104.html