第一节课讲了HTML和CSS,第二节课讲了TCP/IP,本节我们继续讲TCP,以及HTTP。
上课之前先给大家打预防针,这个做网站系列教程注重的是原理,将来不管是做一个网络留言板,还是做一个google/facebook,都能派上用场。
现在网上很多所谓教程,敲几下键盘就搭好一个wordpress/nodebb,虽然快速地实现了目标,但是遇到问题的时候没办法灵活变通。授人以鱼,不如授人以渔;授人以渔,不如授人与海洋生物学。
除此之外你可能会觉得,讲这么多原理性的东西不如直接看教材。其实这点我是同意的,我也希望人手一本《计算机网络》《操作系统原理》,从此人间不再有饥饿和痛苦。
言归正传。如第二课所说,要通过TCP协议通信,一个应用程序首先要“占用”本地操作系统的一个“端口”,然后:
- 如果你要连接别人,就向别人的IP地址和端口,发起一个TCP连接请求
- 如果你要等别人来连接你,就“监听”(等待)其他人向你发起的TCP连接请求
这些功能(发起TCP请求、监听TCP请求)都是由操作系统提供,我们只需要在编写程序的时候,“调用”操作系统的这些功能。至于具体怎么与网卡沟通、怎么按照端口号分发数据包,都是操作系统的工作,不需要我们插手。
Socket(套接字)
我们的应用程序需要与操作系统沟通,告诉操作系统我们需要占用端口、监听TCP连接。那具体怎么沟通呢(有哪些系统功能可以调用、调用参数分别是什么、代码怎么写)?
伯克利在他们自研的BSD操作系统中,提出了一套“沟通方式”,或者叫“API(应用程序编程操作界面,‘如何用C语言告诉操作系统你想干嘛’)”,用来做“TCP/UDP通信”这件事,他们把这种沟通方式称为socket(s),后世一般称为Berkeley Sockets https://en.wikipedia.org/wiki/Berkeley_sockets 或者BSD Sockets。
由于Berkeley Sockets实在太好用(相比其他API而言),它最终成为了行业标准,包括在Windows上也可以用sockets进行TCP/UDP通信。
大实验(期末成绩40%)
下面我就通过一个简单的python程序,演示如何通过编程调用系统的socket API,实现占用、监听TCP端口,并向所有连接者返回HTML网页。
import socket
# socket一般译为“套接字”,意思是“通过编程调用操作系统网络通信功能的界面”
HOST = '0.0.0.0'
# 全零地址表示“所有IP地址”,意思是我们要监听所有人向我们发来的连接(不论在他看来,我的IP地址是什么)
PORT = 80
# 我们要占用本机的80端口
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
# 创建一个socket,类型为IP/TCP
s.bind((HOST, PORT))
<span class="hljs-comment"># 向系统申请占用本机的80端口,占用IP地址范围为“所有IP地址”</span>
s.listen()
<span class="hljs-comment"># 向系统申请开始监听到达本机80端口的连接</span>
print(<span class="hljs-string">'listening on'</span>, HOST, PORT)
<span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
<span class="hljs-comment"># 循环接收到达的每一个连接</span>
conn, addr = s.accept()
<span class="hljs-comment"># 从系统收到一个连接。系统向我们提供:连接对象(socket)本身,</span>
<span class="hljs-comment"># 以及对方的IP地址和端口号</span>
print(<span class="hljs-string">'connected from'</span>, addr)
<span class="hljs-comment"># 在命令行终端输出“connected from <连接者的IP地址和端口号>”</span>
received = <span class="hljs-string">b''</span> <span class="hljs-comment"># 收到的所有字节</span>
<span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>:
<span class="hljs-comment"># 循环读取对方发来的数据</span>
data = conn.recv(<span class="hljs-number">1</span>)
<span class="hljs-comment"># 每次读取一个字节</span>
<span class="hljs-keyword">if</span> data:
<span class="hljs-comment"># 如果读取到字节,就把字节合并到收到的所有字节中</span>
received = received + data
<span class="hljs-keyword">if</span> received[<span class="hljs-number">-4</span>:]==<span class="hljs-string">b'\r\n\r\n'</span>:
<span class="hljs-keyword">break</span>
<span class="hljs-comment"># 如果收到的最后四个字节是“回车换行回车换行(表示HTTP请求结束)”,就停止读取</span>
print(<span class="hljs-string">'received:'</span>, received.decode(<span class="hljs-string">'utf-8'</span>))
<span class="hljs-comment"># 在终端输出对方发来的数据(以UTF-8解码)</span>
response = (
<span class="hljs-string">'HTTP/1.0 200 OK\r\n'</span>
<span class="hljs-string">'Content-Type: text/html; charset=utf-8\r\n\r\n'</span>
<span class="hljs-string">'<h1>习近平必须下台</h1>'</span>)
<span class="hljs-comment"># 我们准备发回给对方的回复</span>
print(<span class="hljs-string">'send response:'</span>, response)
<span class="hljs-comment"># 终端输出我们的回复</span>
conn.sendall(response.encode(<span class="hljs-string">'utf-8'</span>))
<span class="hljs-comment"># 把回复内容以UTF-8编码,发回给请求方</span>
conn.close()
<span class="hljs-comment"># 关闭连接(让对方知道我们已经发完回复了)</span>
FAQ:为什么要用python编程语言
因为python在语法上非常简洁。
补充一些题外话。这几年python热度逐渐增加,有很多人说python适合初学者。我认为初学者可以通过python了解编程的一些基本概念,但是如果真的打算做大事,还是应该老老实实学计算机专业的基础课,尤其是操作系统(C语言),然后再回来使用python,才能真正得心应手。
为了运行上面这个python程序,首先我们把代码保存到一个叫做
a.py
的文本文件里面,然后让python读取并运行这个文件。如果你的电脑上没有python,可以去python官网下载安装python3.7版本。
安装python之后,请在windows命令行或者mac终端,
以管理员权限
(通常出于安全考虑,1024以下的端口都需要系统权限才可以占用)运行
python a.py
。运行命令后,你应该会看到:
listening on 0.0.0.0 80
表示我们已经占用了本机的80端口(HTTP协议默认端口),监听来自所有IP地址的连接(上节课讲过,一台机器可以拥有超过一个IP地址,比如公网IP和内网IP)。
然后我们打开浏览器,访问
http://127.0.0.1/
(这个地址指向本机),会看到一个网页,上面写着:
习近平必须下台
这个时候命令行会显示:
connected from ('127.0.0.1', 64663) # 意思就是收到本机64663端口发起的TCP连接
# 收到对方发来的内容如下
received: GET / HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
# 我们给对方的回复如下
send response: HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
<h1>习近平必须下台</h1>
通过这个实验,我们就知道,浏览器访问一个网站的步骤是:
- 随机挑选一个端口(在这个例子中是64663端口),占用这个端口,然后向对应网站的IP地址(这个例子中是本机127.0.0.1)的80端口,发起一个TCP连接
-
TCP连接建立后,向对方发送
GET / HTTP/1.1...
这样一条信息,意思是我要获取路径 / 的内容,我的协议版本是HTTP1.1版,其他附加信息如下……
-
等待对方回复。我们的回复是
HTTP/1.0 200 OK...
意思是我的协议版本是HTTP1.0版,状态码200(返回数据),其他附加信息如下,网页内容如下……
- 浏览器收到我们的回复,把HTML内容显示在屏幕上。
要停止这个(无限循环运行的)python程序,需要在命令行/终端,按下键盘的
ctrl+c
。
在上面的实验过程中,我们占用了80端口,而占用80端口需要系统管理员权限,如果我们占用一个数字比较大的端口(比如56789),就不需要管理员权限。
小实验1
将上面代码中的80修改为56789,然后再运行一次
python a.py
。
这个时候我们用浏览器就无法访问
http://127.0.0.1/
了,但我们可以访问
http://127.0.0.1:56789/
。
这个实验说明,建网站不一定要占用80端口,只不过因为80端口是HTTP默认端口,浏览器在输入网址的时候,可以省略这个端口号不写。如果用其他端口就必须写明端口号。
小实验2
你家不止一台电脑吧?
如果你的电脑的内网IP地址是192.168.1.35,而你妈妈的电脑(或者手机)跟你连接的是同一个WiFi/路由器,那么她就可以通过你的内网IP地址,来访问你的网站。
假设你已经做完了上面的小实验1,现在用你妈妈的电脑(或者手机)的浏览器访问
http://192.168.1.35:56789/
,就能看到主席同志的问候了。
如果提示不能访问,请把你电脑上的Windows防火墙完全关掉。
至此,我们就成功地在内网架设了一个可以让内网的其他用户访问的、涉嫌煽动颠覆国家政权的网站啦。
HTTP协议
刚才我提了很多次HTTP,但是没有解释HTTP是什么。
浏览器和服务器之间,可以通过TCP连接通信,这个我们刚才实验已经做过了。但是通信总得有个协议,或者叫规范、标准,比如说你发过来的信息必须是什么格式,我发回去的东西是什么格式,大家都按照格式来,不能随便乱发。细心的同学肯定注意到了,我在刚才的python程序里面,就用到了一些这样的格式,比如
HTTP/1.0 200 OK
这行字,就是按照HTTP协议规定的格式。如果不按这个格式,浏览器就没办法接收的,不信你可以改几个字再试一下。
HTTP协议是一个既简单又复杂的协议,它的简单体现在它是通过英语表达意思,人类可以很容易看懂。它的复杂体现在它规定的功能和实现细节非常丰富(各位可以去翻一下HTTP协议的文档)。
如果一个TCP连接,双方都遵守HTTP协议,我们就称这个连接为HTTP连接。一个网页浏览器,访问一个网站,从旁观者的角度来看,就是浏览器通过无数个HTTP连接,从网站获得了许多资源(比如HTML/CSS/JS/图片/视频),最后把这些资源组合成一个网页,显示到计算机屏幕上。
小实验3
在电脑浏览器(比如Chrome Firefox TorBrowser)里打开2047主页,然后在页面上空白地方,点右键,在菜单中点“检查(Inspect)”,打开开发者面板。
在开发者面板中选择“网络(Network)”选项卡,然后刷新页面,可以看到下面这样的提示:
左下角显示,总共发起了53个request(HTTP请求),其中绝大多数是各位的头像图片,总共通过网络传输了22.9kB的内容。右下角可以看到HTTP的请求和响应的具体内容,跟我们刚才python程序收到的非常相似。
通过这个实验我们知道,办网站的本质,就是通过一个计算机程序,以响应外部HTTP连接的方式,向位于其他IP地址上的计算机返回各种资源。
作为2047站长,我的工作的本质,也就是写一个python程序,接收各位浏览器发过来的HTTP连接,并返回相应的资源。
前三节课小结
IP协议解决的是,一个网络里面,计算机之间怎样才能互相发数据且互不干扰。
TCP协议是建立在IP协议之上的,它解决的是,如果一台计算机有很多应用程序、需要发出很多网络连接,怎样让这些连接之间互不干扰。
HTTP协议是建立在TCP协议之上的,它解决的是,如果计算机上的程序,想要通过TCP连接获取另一台计算机上的资源(比如网页和图片之类的),应该遵守怎样的一个通信格式,以确保全世界的浏览器、服务器都能互相兼容。同一个网站之所以能用不同的浏览器打开,正是因为所有浏览器都(某种程度上)遵守HTTP协议。
HTML是一种用来描述网页的语言,CSS是用来辅助排版上色的语言,它们解决的是,一个网页在用户屏幕上究竟要显示成什么样子的问题。它们也有相应的标准,这就是为什么不同的浏览器,显示同一个网页,会有相同(或者近似相同)的效果。
python是一种编程语言,它跟C语言(以及绝大多数编程语言)一样,可以通过代码调用操作系统的功能(API);这些API里面就包括了socket,我们用socket可以告诉操作系统我们想要占用端口、发起或监听TCP连接。
下一节课我会介绍建设HTTP服务器最常见的方案(不是python也不是C,而是Apache/Nginx),以及如何解决内网地址无法从公网访问的问题。