当前位置: 首页 > 后端技术 > Python

使用Python创建Web服务器——第2章:HelloWorld

时间:2023-03-26 00:57:42 Python

从一个HelloWorld程序开始写一个Web服务器,需要用到Python内置库socket。Socket是一个比较抽象的概念,中文叫socket,代表一个网络连接。两台计算机之间的通信大致分为三个步骤:建立连接、传输数据、关闭连接。套接字库为我们提供了这种能力。按照国际惯例,我们将从编写一个HelloWorld程序开始Web服务器的学习。首先创建基于TCP的socket对象:#importsocketimportsocket#createsocketobjectsock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)socket.socket()方法用于创建socket对象。同时我们给它传递了两个参数:socket.AF_INET表示使用IPv4协议,socket.SOCK_STREAM表示这是一个基于TCP的socket对象。这两个参数也是默认参数,可以省略。HTTP协议基于请求-响应模型。请求只能由客户端发起,服务器响应。服务器没有主动发起请求的能力,需要被动等待客户端的请求。所以现在我们有了socket对象,接下来要做的就是监听客户端的请求:#绑定IP和端口sock.bind(('127.0.0.1',8000))#开始监听sock。listen(5)socket对象的bind方法用于绑定监听IP地址和端口。它接收一个由IP和端口组成的元组作为参数。127.0.0.1代表本机IP,只有本机运行的浏览器才能连接。允许的端口号范围在0到65535之间,但小于1024的端口号需要管理员权限才能使用。sock.listen(5)用于开启监听,指定最大等待连接数为5。开启监听后,可以等待接收客户端的请求:client,addr=sock.accept()sock。accept()将阻塞程序并等待客户端的连接。一旦有客户端连接,就会分别返回客户端连接对象和客户端的地址。与客户端建立连接后,接下来就是接收客户端发送的请求数据:data=b''whileTrue:chunk=client.recv(1024)data+=chunkiflen(chunk)<1024:breakreceiving客户端请求数据时,需要调用客户端连接对象的recv方法,参数为每次接收到的数据长度。socket通信过程中的数据是Python的bytes类型。这里每次接收1024字节,接收完所有数据后退出循环。收到客户端发送的数据后,需要对数据进行处理,然后返回一个响应给客户端的浏览器:#打印从客户端收到的数据print(f'data:{data}')#发送给客户端响应数据client.sendall(b'HTTP/1.1200OK\r\nContent-Type:text/html\r\n\r\n

HelloWorld

')为简单起见,在收到客户端后终端发送过来的数据直接打印出来,不做进一步的解析和处理。然后服务器将响应数据发送给客户端。发送的数据也是bytes类型。数据根据HTTP协议的规范进行组装。首先是状态行HTTP/1.1200OK,后面是换行符\r\n,然后通过响应头Content-Type:text/html指定响应结果为HTML类型,然后是连续的两个\r\n\r\n,注意因为响应头和响应报文之间有一个空行,所以会有两个连续的\r\n\r\n,最后是响应正文部分

Hello世界

。发送响应数据后,我们需要关闭客户端连接对象和服务端socket对象:#关闭客户端连接对象client.close()#关闭socket对象sock.close()至此,一个HelloWorld服务端程序有已经写好了,下面是完整的代码:#server.pyimportsocketdefmain():#创建socket对象sock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#允许端口复用sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)#绑定IP和端口sock.bind(('127.0.0.1',8000))#开始监听sock.listen(5)#等待client请求client,addr=sock.accept()print(f'clienttype:{type(client)}\naddr:{addr}')#从客户端接收数据data=b''whileTrue:chunk=client.recv(1024)data+=chunkiflen(chunk)<1024:break#打印从客户端接收到的数据print(f'data:{data}')#将响应数据发送给客户端client.sendall(b'HTTP/1.1200OK\r\nContent-Type:text/html\r\n\r\n

你好世界

')#关闭客户端连接对象client.close()#关闭socket对象sock.close()if__name__=='__main__':main()将上面的代码写入到server.py文件中。然后在终端中使用Python运行此文件:python3server.py。打开浏览器,在地址栏输入http://127.0.0.1:8000,会得到如下结果:HelloWorld!浏览器成功呈现来自服务器的响应。回到终端查看打印出来的客户端请求信息:可以发现,客户端连接对象其实是一个socket对象,客户端IP地址为127.0.0.1,端口为50510。最后是客户端请求数据,只有请求行和请求头,因为没有请求体,所以以两个连续的\r\n\r\n结尾。细心的读者可能已经发现,在最后给出的完整的HelloWorld程序代码中,在创建socket对象之后有一行:sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)这行代码的作用是之前没有介绍过,其实它的作用就是让端口复用。如果不写这行代码,那么当程序运行完毕需要立即重启程序时,程序会抛出异常,因为最后一个端口还被占用,该端口会被释放,允许使用一段时间后。有了这行代码,就不会出现这个问题,方便调试。上面,我们实现了一个简单的服务器程序,可以返回HelloWorld。让服务器永远运行上面实现的HelloWorld服务器程序运行一次就退出了。通常,服务器端程序是永久运行的程序。因为你不知道客户端什么时候发出请求,所以服务器需要一直处于监听状态。只有这样才能保证客户端发出的任何请求都能被服务端接收到。#server_forever.pyimportsocketdefmain():#创建套接字对象sock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#允许端口复用sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)#绑定IP和端口sock.bind(('127.0.0.1',8000))#开始监听sock.listen(5)whileTrue:#等待客户端请求client,addr=sock.accept()print(f'clienttype:{type(client)}\naddr:{addr}')#从客户端接收数据data=b''whileTrue:chunk=client.recv(1024)data+=chunkiflen(chunk)<1024:break#打印datareceivedfromclientprint(f'data:{data}')#将响应数据发送给客户端client.sendall(b'HTTP/1.1200OK\r\nContent-Type:text/html\r\n\r\n

HelloWorld

')#关闭客户端连接对象client.close()if__name__=='__main__':main()在上面的程序中加入了一个whileTrue死循环,经过处理一个客户端连接对象,程序立即执行到下一个循环,并开始等待新的客户端连接,从而实现服务器程序的永久运行。并删除main函数最后一行的sock.close()代码,因为程序要一直运行下去,所以没有必要关闭服务器端的socket连接。将以上代码保存到server_forever.py文件中,在命令行终端使用Python运行程序。浏览器可以多次刷新页面,HelloWorld依然可以正常加载。但是此时如果在终端上查看打印信息,你会发现每次刷新浏览器,浏览器并不是一次只发送一个请求,而是发送两个请求。打开Chrome控制台查看Network,果然浏览器发送了两个请求。第一个请求路径是/,根据浏览器请求和响应记录预计。第二个请求路径是/favicon.ico,这个请求的响应结果也是

HelloWorld

。其实这个请求是由Chrome浏览器自己发起的,用于获取网站图标。当在浏览器中打开京东网站首页时,京东网站的图标将加载到浏览器的标签栏中。我们自己写的HelloWorld服务器并没有返回正确的图标文件,而是返回了一个

HelloWorld

字符串,所以浏览器无法将其识别为图标。最终HelloWorld页面的标签栏中不会出现类似京东网站的图标。这个问题我们暂时不用关心,等我们实现TodoList程序的时候再解决。有的读者可能会疑惑为什么HelloWorld服务器返回的是一个不完整的HTML页面,只是一个带有h1标签的字符串

HelloWorld

,浏览器就可以正常渲染页面,并且响应HelloWorld要加粗。这其实是Chrome浏览器的一种容错机制。如果它检测到HTML标签不完整,它会自动补全缺失的标签。为了更好的渲染。现在如果要结束服务器程序,只需要在程序运行的终端按下组合键Ctrl+C即可。让服务器同时支持多个客户端连接。我们现在实现的HelloWorld服务器程序是单线程的,所以服务器一次只能处理一个请求。但是,我们使用的京东等网站实际上有很多客户端同时连接。如果一次只能处理一个请求,客户端体验会很差。为了让我们的程序能够同时支持多个客户端连接,需要改成多线程版本。#threading_server_forever.pyimportsocketimportthreadingdefprocess_connection(client):"""Processclientconnection"""#从客户端接收数据data=b''whileTrue:chunk=client.recv(1024)data+=chunkiflen(chunk)<1024:break#打印从客户端收到的数据print(f'data:{data}')#将响应数据发送给客户端client.sendall(b'HTTP/1.1200OK\r\nContent-Type:text/html\r\n\r\n

HelloWorld

')#关闭客户端连接对象client.close()defmain():#创建套接字对象sock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#允许端口复用sock.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)#绑定IP和端口sock.bind(('127.0.0.1',8000))#开始监听sock.listen(5)whileTrue:#等待客户端请求client,addr=sock.accept()print(f'clienttype:{type(client)}\naddr:{addr}')#创建一个新的线程来处理客户端connectiont=threading.Thread(target=process_connection,args=(client,))t.start()如果__name__=='__main__':main()改成多线程版本后,服务端每收到一个客户端连接,就会交给新的子线程处理,主线程继续执行,直到下一轮等待用于新的客户端连接。实现了服务端同时支持多个客户端连接。本章通过编写一个HelloWorld程序,学习了Web服务器的开发。如果你是编程新手,对socket编程理解起来还是有点吃力,那么可以通过Python文件操作对比学习。文件处理通常是三个步骤:打开文件、读写数据、关闭文件。利用现有知识举一反三学习新技术也是一种很好的方法。本章源码:chapter2联系我:微信:jianghushinian邮箱:jianghushinian007@outlook.com博客地址:https://jianghushinian.cn/