python网络编程 -- 建立一个http服务器+twisted模块--part.1
=>
B/S和C/S架构、 后者使用TCP协议,前者使用HTTP协议(HTTP是对TCP的扩充)。
所有的网络通讯都必须经过OSI(七层架构) -- 七层架构详解 => TCP/IP 四层架构 、五层架构
但是为了方便程序开发,socket便出现了,他包装了七层架构中对数据的处理 ,让开发只专注于上层,而不用去为了数据传输和接收为每一个架构的数据处理费心。socket是对各种网络协议的抽象实现。不同语言为了方便开发,都会对网络协议进行包装,因此socket是一个通用的概念。
=>
socket是不同进程之间的通讯,这意味着不仅能进行客户机和服务器之间,同一台主机之间的不同进程也可以通过socket进行交流。
socket主要是对TCP/IP协议和UDP协议的包装:
TCP/IP : 有状态、三次握手、四次挥手、性能较低资源占用大;
UDP : 无状态、没有握手与挥手、不保存单个结点连接信息、适合广播操作
总 -> 不管是TCP还是UDP,都是对传输层的保证,数据都会通过七层架构进行处理,最后经过物理层发出。socket的存在包装了传输层,因此现在程序员开发的时候就只需要关注核心带吧,不需要在注意具体的操作协议。
=>
TCP通讯,C/S架构
- 服务器端socket程序
import socket
SERVER_HOST = 'localhost' #服务器端
SERVER_PORT = 7000 #本程序端口
def main():
with socket.socket() as server_socket:
server_socket.bind((SERVER_HOST,SERVER_PORT)) #绑定本机端口
server_socket.listen() #开启监听
print(f'服务器启动完毕,在{SERVER_PORT}端口监听,等待客户端链接...')
#进入阻塞 直到客户端进行链接后解除
client_conn,address = server_socket.accept() #进入阻塞状态、
with client_conn:
print(f'客户端已连接到服务端,主机地址是{address[0]},端口是{address[1]}')
client_conn.send("请求已经收到,测试成功!".encode('UTF-8'))
if __name__ == '__main__':
main()
- 使用telnet命令测试
windows需要在设置里配置telnet,他是操作系统中提供的一个测试命令。
telnet localhost 7000
- 客户端socket程序
import socket
SERVER_HOST = '127.0.0.1' #服务器主机名称/ip地址
SERVER_PORT = 7000 #服务器链接端口
def main():
with socket.socket() as client_socket: #建立客户端socket
client_socket.connect((SERVER_HOST,SERVER_PORT))
print(f'服务器返回数据 -- {client_socket.recv(40).decode("UTF-8")}')
if __name__ == '__main__':
main()
=>
echo程序模型
- echo服务端
import socket
SERVER_HOST = 'localhost'
SERVER_PORT = 7070
coding = ['utf-8','gbk']
def echo_server():
with socket.socket() as server_socket:
server_socket.bind((SERVER_HOST,SERVER_PORT)) #bind()函数传入元组
server_socket.listen() #监听端口
print('服务端已启动,等待客户端链接...')
socket_conn,addr = server_socket.accept() #等待接收 进入阻塞
with socket_conn: #在接收到的信息前添加【Echo】然后返回
#连接上了之后才while循环 不断进行通讯
#第一次连接成功发送一次提示
socket_conn.send('【Echo】连接成功,输入字符发送请求,输入byebye结束链接。'.encode('utf-8'))
while True:
response = socket_conn.recv(100).decode(coding[0])
if response.upper() == "BYEBYE":
print('客户端发送终止指令,连接结束...')
socket_conn.send('byebye'.encode(coding[0]))
break
else:
socket_conn.send(f'【Echo】{response}'.encode(coding[0]))
if __name__ == '__main__':
echo_server()
这里需要注意的有两点(我犯的错😣),一是
with as
的时候需要注意命名不要起冲突;二是再注意while的位置,连接成功后再while才能实现不断地通讯,而不是在连接之前就不断地while。
这里省略telnet测试。
- echo客户端
import socket
SERVER_HOST = 'localhost'
SERVER_PORT = 7070
def echo_client():
with socket.socket() as client:
client.connect((SERVER_HOST,SERVER_PORT))
#连接成功之后进行通讯
while True:
response = client.recv(100).decode('utf-8')
if response.upper() == 'BYEBYE':
print('链接结束...')
break
else:
print(response)
text = input()
client.send(text.encode('utf-8'))
if __name__ == '__main__':
echo_client()
需要注意,一个服务只能绑定一个端,如果程序端口被占用,那么就无法正常启动。
此时的程序已经实现了一个socket通讯,并且是基于TCP协议的,但是有一个重大问题:采用的是单进程的处理模型完成的通讯。这就意味着,如果当前服务端已经有一个客户端进行链接,那么另一个客户端链接的话就会因为主进程被占用而导致无法操作。因此提高性能就需要进行多进程优化。同时,当前服务端程序还会在客户端断开连接之后停止运行,这还意味着后一个服务端并不会像队列一样依次接收客户端,而是在第一个客户端完成连接之后关闭服务,导致后面的客户端失去连接。
3. 修改服务端程序,采用多进程 (多并发编程)
import socket,multiprocessing #引入多进程处理
SERVER_HOST = 'localhost'
SERVER_PORT = 7070
coding = ['utf-8','gbk']
def echo_handle(socket_conn,addr):
with socket_conn: #在接收到的信息前添加【Echo】然后返回
#连接上了之后才while循环 不断进行通讯
#第一次连接成功发送一次提示
socket_conn.send('【Echo】连接成功,输入字符发送请求,输入byebye结束链接。'.encode('utf-8'))
while True:
response = socket_conn.recv(100).decode(coding[0])
if response.upper() == "BYEBYE":
print(f'客户端-{addr[1]}发送终止指令,连接结束...')
socket_conn.send('byebye'.encode(coding[0]))
break
else:
socket_conn.send(f'【Echo】{response}'.encode(coding[0]))
def echo_server():
with socket.socket() as server_socket:
server_socket.bind((SERVER_HOST,SERVER_PORT)) #bind()函数传入元组
server_socket.listen() #监听端口
print('服务端已启动,等待客户端链接...')
while True:
socket_conn,addr = server_socket.accept() #等待接收 进入阻塞 因此在没有接收到客户端请求的时候while会停止在这里 接收到一个循环一次
process = multiprocessing.Process(target=echo_handle,args=(socket_conn,addr),name=f'客户端进程-{addr[1]}')
process.start() #启动进程
if __name__ == '__main__':
echo_server()
PS.高并发 -> 处理好服务端的处理效率。
这里需要注意的是:一需要导入multiprocessing
模块处理多进程;二需要将处理函数单独剥离出去,然后根据每一个请求创建一个进程响应;三multiprocessing.Process
实例化参数中target
表示目标函数,args
表示传入进函数的参数,name
表示进程的名字。四还需要注意的是,在进入accept
的时候,主进程会进入阻塞,这意味着外边的while循环会暂停在accept()
这里,直到接受到后才进行下一个循环,也就是进入下一个阻塞等待,这也解决了修改之前服务端会在一个客户端终止连接之后结束服务,他会一直运行。
#=>
UDP通讯
相对于TCP是一种不安全连接,但是想能相对较好。在python使用中差别不大,只需要在引用socket中指定对应参数,同时也不需要监听、接收修改为recvfrom()、发送修改为sendto()。
- UDP服务端
import socket
SERVER_HOST = 'localhost'
SERVER_PORT = 7070
def main():
#socket.AF_INET表示使用ipv4网络协议进行服务端创建
#socket.SOCK_DGRAM 创建一个数据报(UDP) 协议的网络端
with socket.socket(socket.AF_INET,socket.SOCK_DGRAM) as udp_server:
udp_server.bind((SERVER_HOST,SERVER_PORT)) #bind()函数传入元组
print(f'服务器启动完成,监听端口{SERVER_PORT}')
while True: #服务端响应就是在收到的信息前面添加【Echo】
data,addr = udp_server.recvfrom(100) #不断接收客户端信息
print(f'【服务器】客户端{addr[0]}:{addr[1]}成功连接!')
echo_data = f'【Echo】{data.decode("utf-8")}'.encode('utf-8')
udp_server.sendto(echo_data,addr) #将内容相应给接收到信息的对应的客户端
if __name__ =='__main__':
main()
- UDP客户端
import socket
SERVER_HOST = 'localhost'
SERVER_PORT = 7070
#UDP客户端与TCP客户端的不同就在于 一是协议不同 二是不需要进行连接 只需要不断地发送接收即可
def echo_udp_client():
with socket.socket(socket.AF_INET,socket.SOCK_DGRAM) as client:
while True:
send_data = input('请输入想发送的数据...\n')
if send_data:
client.sendto(send_data.encode('utf-8'),(SERVER_HOST,SERVER_PORT))
data,addr = client.recvfrom(100) #recvfrom会接受一个元组 包含两个元素data和主机地址 而主机地址又是一个元组 包含主机地址和端口两个元素
print(data.decode('utf-8'))
else: #如果输入内容为空 那么程序结束
break
if __name__ == '__main__':
echo_udp_client()
- send/sendto, recv/recvfrom区别
PS. 1. UDP不需要建立稳定的链接 ,不受到连接的控制,不需要考虑多连接(不需要考虑并发),需要不断的接收,但只需要将响应的信息原路返回给对应的客户端即可。2. UDP是一种无连接协议,因此不能使用telnet
命令进行连接测试。
=>
UDP广播
- UDP广播接收端
import socket
BROASTCAT_CLIENT_ADDR = ('0.0.0.0',21567) #客户端绑定地址
def mian():
with socket.socket(socket.AF_INET,socket.SOCK_DGRAM) as client: #这里不变
#设置广播模式
client.setsockopt(socket.SOL_SOCKET,socket.SO_BROADCAST,1)
client.bind(BROASTCAT_CLIENT_ADDR) #绑定广播客户端的地址
while True:
message,addr = client.recvfrom(100)
print(f'接收到的广播客户端数据:【{message.decode("utf-8")}】,广播来源为{addr[0]}:{addr[1]}')
if __name__ == '__main__':
mian()
- UDP广播发送端(服务端)
import socket
BROASTCAT_SERVER_ADDR = ('<broadcast>',21567) #广播地址
def main():
with socket.socket(socket.AF_INET,socket.SOCK_DGRAM) as server_socket: #这里不变 设置UDP编码
#设置广播模式
server_socket.setsockopt(socket.SOL_SOCKET,socket.SO_BROADCAST,1)
server_socket.sendto(f'这是一条来自服务端的广播...'.encode('utf-8'),BROASTCAT_SERVER_ADDR)
if __name__ == '__main__':
main()
- 需要注意的点/具体解释
关键的函数配置项:
setsockopt(self,level:int,optname:int.value:Union[int,bytes])
level: 设置选项所在的协议层编号,有四个可用配置项
socket.SOL_SOCKET 基本嵌套字接口
socket.IPPROTO_IP ipv4嵌套字接口
socket.IPPROTO_IPV6 ipv6嵌套字接口
socket.IPPRPTP_TCP TCP嵌套字接口
optname : 设置选项名称,例如,如果要进行广播则可以使用“socket.BROADCA“;
value: 设置选项的具体内容
为什么要设置广播的端口呢?
"广播有一个广播组,即只有一个广播组内的节点才能收到发往这个广播组的信息。什么决定了一个广播组呢,就是端口号,局域网内一个节点,如果设置了广播属性并监听了端口号A后,那么他就加入了A组广播,这个局域网内所有发往广播端口A的信息他都收的到。在广播的实现中,如果一个节点想接受A组广播信息,那么就要先将他绑定给地址和端口A,然后设置这个socket的属性为广播属性。"
reference
可以理解为,服务端向同一个局域网内的所有设备的这个端口号发送消息,然后只有接收端设置为广播模式并绑定这个端口之后,才能接收到客户端向着个端口发送的消息;而所有的局域网内的这个端口的设备就组成了一个广播组。具体设置来说,服务端需要设置这个广播组的端口和广播模式:
BROASTCAT_SERVER_ADDR = ('<broadcast>',21567) #广播地址
......
server_socket.setsockopt(socket.SOL_SOCKET,socket.SO_BROADCAST,1) #设置广播模式
server_socket.sendto(f'这是一条来自服务端的广播...'.encode('utf-8'),BROASTCAT_SERVER_ADDR) #发送到广播组的这个端口
而客户端需要设置广播来源的地址和广播发送的端口即可:
BROASTCAT_CLIENT_ADDR = ('0.0.0.0',21567) #客户端绑定地址
......
client.setsockopt(socket.SOL_SOCKET,socket.SO_BROADCAST,1) #
client.bind(BROASTCAT_CLIENT_ADDR) #绑定广播客户端的地址
广播接收端不一定能接收到广播,但是只要打开接收端就可以接收到广播;客户端执行之后就会一直等待服务端发送的消息。
=>
HTTP协议/HTTP服务器
传统socket需要提供两个程序端(服务端、客户端),因此每一次服务端升级都需要进行客户端强制更新。因此在传统网络编程的基础上就实现了HTTP协议(HTTP是对TCP协议的一种更高级的包装),但是并没有完全脱离传统的TCP协议,是在TCP协议前面添加的头部信息。
HTTP - 超文本传输协议 是对B/S架构的标准协议
B/S相对于C/S的好处就是不用在开发一套客户端
在整个http开发流程中,最重要的设计就是html代码的开发。但对于web服务器开发而言,最重要的是清楚http服务器的开发。
在整个http请求和响应的处理过程中,关键问题就在于请求和响应的头部信息有哪些、响应状态码。
HTTP协议中,设计了多种请求模式,主要有get、post。
http头部信息:
http状态码:1**、2**、3**、4**、
常见响应头信息