文章
技术

我亲自教大家做网站(3)

thphd  ·  2020年10月16日 2047前站长

第一节课讲了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 &lt;连接者的IP地址和端口号&gt;”</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">'&lt;h1&gt;习近平必须下台&lt;/h1&gt;'</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>

通过这个实验,我们就知道,浏览器访问一个网站的步骤是:

  1. 随机挑选一个端口(在这个例子中是64663端口),占用这个端口,然后向对应网站的IP地址(这个例子中是本机127.0.0.1)的80端口,发起一个TCP连接
  2. TCP连接建立后,向对方发送 GET / HTTP/1.1... 这样一条信息,意思是 我要获取路径 / 的内容,我的协议版本是HTTP1.1版,其他附加信息如下……
  3. 等待对方回复。我们的回复是 HTTP/1.0 200 OK... 意思是 我的协议版本是HTTP1.0版,状态码200(返回数据),其他附加信息如下,网页内容如下……
  4. 浏览器收到我们的回复,把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),以及如何解决内网地址无法从公网访问的问题。

菜单
  1. 琳不可瑤混 小朋友
    琳不可瑤混   你們可不能混瑤哦!

    謝謝站長~

  2. dreamMen1  

    谢谢站长

    • 求教前端js部分和框架。
    • 求教cms系统的自定义。

    我也好想做出2047这么优秀的网站

  3. Salamander  

    站长的python代码里出现了很多html的span标记,并且个别行存在缩进问题。望站长再接再厉,期待《亲教(4)》,比心。