一、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() 套接字的文件描述符
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>
执行结果
接口返回:
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
参考地址: