目录
第一章 绪论
本文作为记录学习过程的笔记,以《计算机网络——自顶向下的方法》为参考教材。
待续。。。
第二章 应用层
URI:URI是由某个协议方案表示的资源的定位标识符,绝对URI指涵盖全部必要信息。格式如:
URL:区分URI,可以说URL是URI的一个子集,指资源在互联网上的地址。
cookie:待续。。。
C/S模式:
P2P模式:
套接字实验
实验一:用python开发一个简单的Web服务器,它仅能处理一个请求:
- 当一个客户(浏览器)联系时创建一个套接字;
- 从这个连接接收HTTP请求;
- 解释该请求以确定所请求的特定文件;
- 从服务器的文件系统获取请求文件;
- 创建一个由请求的文件组成的HTTP响应报文,报文前面由首部行;
- 经TCP连接向请求的浏览器发送响应,如果浏览器请求的文件不存在服务器中,返回“404 NOT FOUND”的差错报文。
从网站渠道获取相关代码,但调试过程遇到各种报错。
问题1:[WinError10061]由于目标计算机积极拒绝,无法连接。
该问题分析是本机的网络安全设置阻止了非法连接,我没找到在哪个设置放行连接,因此作罢,改用浏览器作为客户端请求。
问题2:[WinError10038]在一个非套接字上尝试了一个操作。
该问题是指在套接字不存在后进行的‘recv’接收,因此无法进行,该原因是代码中:
serverSocket = socket(AF_INET,SOCK_STREAM)
serverSocket.bind(("127.0.0.1",9999))
serverSocket.listen(1) #听一个连接用户
connectionSocket, addr = serverSocket.accept()
while True:
print('wait for connection..')
try:
data = connectionSocket.recv(1024) #接受1K的数据
print(data) #data是一个get的http报文
if not data:
continue
filename = data.split()[1]
f = open(filename[1:],encoding=("utf-8"))
outputdata = f.read()
outputdata = 'HTTP/1.1 200 OK\r\nServe:MakeByMyself\r\nConten-Type:HTML\r\n\r\n'+ outputdata #接收以后回复报文
#connectionSocket.send(header.encode())
for i in range (0,len(outputdata)):
connectionSocket.send(outputdata[i].encode())
connectionSocket.close()
connectionSocket, addr = serverSocket.accept() 不在while循环体内,而每次while循环都会connectSocket.close()关闭套接字,因此后续操作会产生在非套接字上尝试了一个操作的报错。将connectionSocket, addr = serverSocket.accept() 放到while循环体内即可解决。
问题3:显然浏览器中的报文比编译器中的工整好看,但却缺少了‘Response Headers’
该问题主要因为服务器response报文只有“HTTP/1.1 200 OK”一个状态,浏览器不显示在response报文中,主动添加response报文格式即可。
最终运行下列代码:
from socket import *
serverSocket = socket(AF_INET,SOCK_STREAM) #建立socket套接口对象
serverSocket.bind(("127.0.0.1",9999)) #使用bind函数将主机地址和端口号绑定
serverSocket.listen(1) #单线程,等待一个连接用户
#connectionSocket, addr = serverSocket.accept() 注释掉
while True:
print('wait for connection..')
connectionSocket, addr = serverSocket.accept() #接受连接
try:
data = connectionSocket.recv(1024) #接受1K的数据
print(data) #data是一个get的http报文
if not data: #等待数据传输
continue
filename = data.split()[1] #将请求数据分割出文件名,[1]取数组第一行
f = open(filename[1:],encoding=("utf-8")) #打开文件
outputdata = f.read() #读出数据结合response报文一起传
outputdata = 'HTTP/1.1 200 OK\r\nServe:MakeByMyself\r\nConten-Type:HTML\r\n\r\n'+ outputdata #接收以后回复报文
for i in range (0,len(outputdata)): #循环调用send()将数据传到客户端
connectionSocket.send(outputdata[i].encode())
connectionSocket.close() #关闭连接
#\r\n是区分转行,而\r\n\r\n是报文结束标志
except IOError: #请求文件不存在,报404错误
outputdata = 'HTTP/1.1 404 NOT \r\nFOUNDServe:MakeByMyself\r\nConten-Type:HTML\r\n\r\n'
for i in range(0, len(outputdata)):
connectionSocket.send(outputdata[i].encode())
connectionSocket.close()
得到报文,并分析报文:
报文分析:
HTTP的请求报文由3部分组成(请求行、请求头、请求体),同理其响应报文也由3部分组成(响应行、响应头、响应体)。
请求行:包括①request method,此处用‘GET’方法,GET和POST是最常见的HTTP方法;
②request URL;③协议名称及版本号。
请求头:包含下图所有内容
- Accept 请求报文通过一个Accept报文属性告诉服务器,客户端接收什么类型的响应;Accept属性的值可以为一个或多个MIME类型的值,
MIME格式:大类型/小类型[;参数] Accept:text/plain
比如:text/html,html文件 text/css,css文件 image/*,所有图片文件
- Accept-Encoding: 声明浏览器接受的压缩编码类型,如gzip, deflate
- Accept-Language:浏览器通知服务器,浏览器支持的语言。各国语言(国际化i18n)
- Cache-Control:用于 HTTP 请求和响应中的,通过指定指令实现缓存机制。缓存指令是单向的,意味着在请求中设置的指令,不一定被包含在响应中。其参数有
public:响应可以被任何对象(客户端、代理服务器等)缓存
no-cache:强制要求缓存把请求提交给原始服务器进行验证(协商缓存验证)
no-store:不使用任何缓存
max-age:缓存最大周期
must-revalidate:一旦资源过期,成功向原始服务器验证之前,不能缓存响应后的任何数据
- User-Agent:浏览器通知服务器,客户端浏览器与操作系统相关信息
- ser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) uChrome/69.0.3497.100 Safari/537.36
- Connection:表示客户端与服务连接类型;Keep-Alive表示持久连接,close已关闭
- Content-Length:请求体的长度
- Host:请求的服务器主机名
- Content-Type:请求的与实体对应的MIME信息。如果是post请求,会有这个头,默认值为application/x-www-form-urlencoded,表示请求体内容使用url编码
- Sec-Fetch-Dest:表示请求的目的地,即如何使用获取的数据;
- Sec-Fetch-Mode:该请求头表明了一个请求的模式;
取值范围:cors
:跨域请求;no-cors
:限制请求只能使用请求方法(get/post/put)和请求头(accept/accept-language/content-language/content-type);same-origin
:如果使用此模式向另外一个源发送请求,显而易见,结果会是一个错误。你可以设置该模式以确保请求总是向当前的源发起的;navigate
:表示这是一个浏览器的页面切换请求(request)。 navigate请求仅在浏览器切换页面时创建,该请求应该返回HTML;websocket
:建立websocket连接; - Sec-Fetch-Site:
表示一个请求发起者的来源与目标资源来源之间的关系;
取值范围:
cross-site
:跨域请求;same-origin
:发起和目标站点源完全一致;same-site
:有几种判定情况,详见说明;none
:如果用户直接触发页面导航,例如在浏览器地址栏中输入地址,点击书签跳转等,就会设置none -
Sec-Fetch-User:取值是一个Boolean类型的值,true(?1)表示导航请求由用户激活触发(鼠标点击/键盘),false(?0)表示导航请求由用户激活以外的原因触发;
取值范围:?0 或
?1
-
Upgrade-Insecure-Requests 请求头向服务器发送一个客户端对HTTPS加密和认证响应良好,若取值:1,则可以升级连接为HTTPS,
请求体:作为向目标服务器提交数据的部分,可以传递请求参数,请求URL也可以通过类似于“/chapter15/user.html? param1=value1¶m2=value2”的方式传递请求参数。
1.响应行
(1)响应的协议和版本号
(2)响应状态码
(3)响应状态描述符
2.响应头
(1)key:value 不同的响应头,有不同含义,此 处 是 空 行
3.响应体
服务器---->> 传给客户端的数据,可以是文字、文件、图片等各种数据类型。
抓包
使用wireshark抓包,利用过滤器筛选出端口号,挑选本机网络回路。
三次握手:
关于三次握手和四次挥手的详解文章有许多,这里选其中一个作为链接。
https://baijiahao.baidu.com/s?id=1654225744653405133&wfr=spider&for=pc
抓包显示的三次握手:
操作59:客户端53415——>服务器9999发送:标记位[SYN]:
- 序号Seq=0设为X;
- Win=65535当前窗口大容纳字节;
- Len=0,即不带数据;
- MSS=65495,MSS表示最大段的最大值,它是65535-20(IP典型包头)-20(TCP典型包头)=65495,也就是说当一次传输数据大于65495时,TCP会将其分段传输。
- WS=256,即原有窗口大小扩容256倍
- SACK_PERM=1: 允许选择确认,SACK信息包含接收方已经成功接收的数据块的序列号范围。而SACK_PERM字段为1表明,选择开启了SACK功能。
操作60:服务器9999——>客户端53415发送[SYN,ACK]:
- Seq=0设为Y,Ack=1(这是上一个Seq,X+1)表示确认可以建立新连接。
操作61:客户端53415——>服务器9999发送[ACK]:
- Seq=1(即X+1),Ack=1(即Y+1),客户端接收到来自服务器端的确认后,明确了从客户端到服务器的数据传输是正常的,并同意连接。
三次握手结束后,操作62:客户端53415——>服务器9999发送[PSH,ACK]:开始传输数据,可见TCP将其封装为PDU即协议数据单元,进行传输,此次数据长度Len=700,可以推测下一个服务器的Ack=X+len=701.
四次挥手:需强调的是,四次挥手不同于三次握手需由客户端发起,它既可以由客户端发起也可以由服务器发起,此实验由服务器发起的挥手动作。
关注重点不在于四次挥手,而是四次挥手后发生的[TCP Retransmission]报错,报错原因是TCP port number reused,即服务器已经把端口号释放,但客户端在请求重传。我初以为是挥手后客户端需要等待2MSI时间,但此等到是在四次挥手期间的,再细看端口号:53409,是比53415前的序列,可以判断是该传输发生错误,等待定时器超时,然后重传,但此次重传时间却在四次挥手以后,故产生此错误。
PS:客户端的端口号是循环使用的,每次传输后端口号+1,到达65535后循环使用。
TCP是可靠的传输连接,但它是在不可靠的传输环境建立可靠连接,那肯定是会产生错误的,TCP也有一系列的维护传输出错的措施。
待续。
套接字实验二 UDPping程序
要求:用python编写一个客户ping程序,该程序:
- 该客户将发送简单的Ping报文,接收一个从服务器返回的pong报文,并确定发送ping报文到接收pong报文为止的时延,记为RTT;
- 标准的ping使用ICMP协议,此时是创建一个非标准的基于UDP的ping程序;
- ping程序向客户端发送10个ping报文,对于每个报文,待其返回pong报文时要计算出相应的RTT;
- UDP是不可靠的协议,传输分组有可能丢失,当客户发送ping报文等待1s无应答则返回请求超时的报文。
UDP是传输层上无连接的传输协议,是不可靠的;但相比TCP协议,它又节省了建立连接的开销。在本实验中可以将UDP客户端程序设置等待1s无应答,则返回等待超时。UDP协议有优缺点,当选择UDP协议传输数据时,利用其时延小的优点,传输丢包是可以接受的,比如:一些不断定时采样的数据,该周期传输丢包,下一周期采样再次传输数据,丢包对应用影响不大,像股票走势图。
服务器端代码
import random
from socket import *
serverSocket= socket(AF_INET,SOCK_DGRAM) #使用IPv4,若用IPv6就AF_INET6
serverSocket.bind(('127.0.0.1',8888)) #绑定主机地址和端口
while True:
rand = random.randint(0,10) #利用随机数决定服务器是否应答
message,address = serverSocket.recvfrom(1024) #recvfrom是UDP的接收方法函数
print("收到来自%s的报文:(%s)"%(address,message)) #打印来自客户端的ping报文
print("随机数是:%d"%rand)
message = message.upper() #把收到的报文转为大写来发送回客户端
if rand < 4: #应答概率为50%
continue
serverSocket.sendto(message,address) #发送大写的报文作为pong报文发送客户端
客户端代码:
import time
from socket import *
serverName = '127.0.0.1'
serverPort = 8888
clientSosket = socket(AF_INET,SOCK_DGRAM)
clientSosket.settimeout(1) #设置超时时间为1s
for i in range(0,10):
oldTime = time.time()
sendTime = time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(oldTime))
#接收时间元组并返回可读字符串作为当地时间,sendtime类型是字符串
message = ('package %d,client_local_time:%s' %(i+1,sendTime)).encode()
#将package i和客户端的时间作为ping报文发送,还要将str类型转为bytes(传输要求)
try:
clientSosket.sendto(message,(serverName,serverPort))
modifiedMessage,serverAddress = clientSosket.recvfrom(1024)
rtt = time.time() - oldTime
modifiedMessage = modifiedMessage.decode("utf-8")
print('报文%d 收到来自 %s 的应答:%s ,往返时延RTT= %fs'%(i+1,serverName,modifiedMessage,rtt))
except Exception as e:
print('报文 %d :的请求超时'%(i+1))
运行代码并结合抓包如图:
从抓包结果亦可见请求报文四、七、九,服务器端是进行答复的。抓包计算的时间差小于代码计算的RTT,是因为抓包的时间是建立UDP协议的时间,没有包括代码运行的时间。
看抓包的报文