️️个人简介:以山河作礼。
️️:Python领域新星创作者,CSDN实力新星认证,阿里云社区专家博主
:Web全栈开发专栏:《Web全栈开发》免费专栏,欢迎阅读!
TCP (Transmission Control Protocol) 是在互联网协议(IP)上的一种基于连接(面向连接)的传输层协议。数据通信的基本要素包括“所传输的数据”
、“发送方”
、“接收方“
三个要素,还有保证所传输的数据是完好无损,正确无误的。这一普遍要求都是由TCP满足的,下面介绍TCP的三次握手和四次挥手过程。
三次握手:
1.客户端发送一个SYN,表示要发起一个连接请求。
2.服务端收到客户端请求之后,向客户端发送一个SYN和ACK,表示确认收到请求并准备好连接。
3.客户端接收到服务端的响应之后,向服务端发回ACK,表示确认可以建立连接了。
这个时候客户端和服务端就建立起了连接,可以开始通信了。
四次挥手:
1.客户端发送一个FIN,表示要释放连接。
2.服务端接收到FIN之后,会回复一个ACK,表示确认收到。
3.服务端准备释放连接时,同样发送一个FIN给客户端。
4.客户端接收到服务端的FIN之后,会回复一个ACK,表示确认收到释放请求。
这个时候,客户端和服务端都释放了连接,连接断开。
总结:
1.三次握手是建立TCP连接的必要步骤,而四次挥手则是释放TCP连接的必要步骤。
2.三次握手和四次挥手的目的是确保数据传输的可靠性和正确性。
3.连接的释放过程比连接的建立过程要复杂,这是因为连接释放过程需要双方都知道连接已经释放,而连接建立过程只需要被请求方知道即可。
4.TCP协议的三次握手和四次挥手是TCP协议可靠性的重要保证,也是网络通信的必要约定。
网络协议
HTTP协议=
HTTPS协议
HTTP协议的主要特点
HTTPS协议的主要特点
请求行
URL字段
HTTP协议的请求方法
GET和POST区别
请求头部
请求头部由关键字/值对组成,每行一对,关键字和值用英文冒号 “ : ” 分隔。
请求头部通知服务器有关于客户端请求的信息,典型的请求头有:
User-Agent:产生请求的浏览器类型。
Accept:客户端可识别的内容类型列表。
Host:请求的主机名,允许多个域名同处一个IP地址,即虚拟主机。
请求正文
请求正文向服务器提交的请求数据,GET请求的参数一般是放在请求行后的键值对,post请求的参数类型多样(表单、json、xml、图片等)
响应行
响应行由响应状态码、响应信息和HTTP协议版本字段3个字段组成
响应状态码
响应状态码由三位数字组成,第一个数字定义了响应的类别,且有五种类型
常见的响应状态码
响应头
响应头用于描述服务器的基本信息,以及数据的描述,服务器通过这些数据的描述信息,可以通知客户端如何处理等一会儿它回送的数据。
常见的响应头:
Content-Length:表示内容长度
Content- Type:表示后面的文档属于什么MIME类型
Server:服务器通过这个头告诉浏览器服务器的类型
响应正文
静态web服务器是指可以为出发请求的浏览器提供静态文档的程序。
平时我们浏览百度新闻数据的时候,每天的新闻数据都会发生变化,那访问的这个页面就是动态的。
而静态的Web服务器,页面的数据不会发生变化。
搭建python自带的静态web服务器使用命令:
python -m http.server
-m:表示运行包里面的模块,执行这个命令时,需要进入指定静态文件的目录,然后通过浏览器就能访问对应的html文件
端口号不指定默认是8000
搭建步骤
在终端首先进入到指定静态文件的目录
输入命令:python -m http.server [端口号] --bind 127.0.0.1,不指定端口号的话默认是8000
在浏览器中访问本地的html文件
首先在命令行进入该文件所在目录,然后启动静态服务器:
最后可以在浏览器进行访问
import socket
if __name__ == '__main__':
# 创建tcp服务端套接字
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口号复用, 程序退出端口立即释放
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 绑定端口号
tcp_server_socket.bind(("", 9000))
# 设置监听
tcp_server_socket.listen(128)
while True:
# 等待接受客户端的连接请求
new_socket, ip_port = tcp_server_socket.accept()
# 代码执行到此,说明连接建立成功
recv_client_data = new_socket.recv(4096)
# 对二进制数据进行解码
recv_client_content = recv_client_data.decode("utf-8")
print(recv_client_content)
with open("static/index.html", "rb") as file:
# 读取文件数据
file_data = file.read()
# 响应行
response_line = "HTTP/1.1 200 OK\r\n"
# 响应头
response_header = "Server: PWS1.0\r\n"
# 响应体
response_body = file_data
# 拼接响应报文
response_data = (response_line + response_header + "\r\n").encode("utf-8") + response_body
# 发送数据
new_socket.send(response_data)
# 关闭服务与客户端的套接字
new_socket.close()
目前的web服务器,不管用户访问什么页面,返回的都是固定页面的数据,接下来需要根据用户的请求返回指定页面的数据
返回指定页面数据的实现步骤:
import socket
def main():
# 创建tcp服务端套接字
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口号复用, 程序退出端口立即释放
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 绑定端口号
tcp_server_socket.bind(("", 9000))
# 设置监听
tcp_server_socket.listen(128)
while True:
# 等待接受客户端的连接请求
new_socket, ip_port = tcp_server_socket.accept()
# 代码执行到此,说明连接建立成功
recv_client_data = new_socket.recv(4096)
if len(recv_client_data) == 0:
print("关闭浏览器了")
new_socket.close()
return
# 对二进制数据进行解码
recv_client_content = recv_client_data.decode("utf-8")
print(recv_client_content)
# 根据指定字符串进行分割,最大分割次数指定2
request_list = recv_client_content.split(" ", maxsplit=2)
# print("===")
# print(request_list)
# print("===")
# 获取请求资源路径
request_path = request_list[1]
print(request_path)
# 判断请求的是否是根目录,如果条件成立,指定首页数据返回
if request_path == "/":
request_path = "/index.html"
try:
# 动态打开指定文件
with open("static" + request_path, "rb") as file:
# 读取文件数据
file_data = file.read()
except Exception as e:
# 请求资源不存在,返回404数据
# 响应行
response_line = "HTTP/1.1 404 Not Found\r\n"
# 响应头
response_header = "Server: PWS1.0\r\n"
with open("static/error.html", "rb") as file:
file_data = file.read()
# 响应体
response_body = file_data
# 拼接响应报文
response_data = (response_line + response_header + "\r\n").encode("utf-8") + response_body
# 发送数据
new_socket.send(response_data)
else:
# 响应行
response_line = "HTTP/1.1 200 OK\r\n"
# 响应头
response_header = "Server: PWS1.0\r\n"
# 响应体
response_body = file_data
# 拼接响应报文
response_data = (response_line + response_header + "\r\n").encode("utf-8") + response_body
# 发送数据
new_socket.send(response_data)
finally:
# 关闭服务与客户端的套接字
new_socket.close()
if __name__ == '__main__':
main()
问题:
Socket有一个缓冲区,缓冲区是一个流,先进先出,发送和取出都可以自定义大小,如果缓冲区的数据未取完,则可能会存在数据堆积。其中recv(1024)
表示从缓冲区里取最大为1024个字节,但实际取值大小是不确定的,可能会导致丢包,socket发送两条连续数据时,也有可能最终会拼接成一条进行发送,所以也会导致粘包问题的产生。
解决的一些办法和思路:
在每条数据发送之间增加停顿时间,如【tiem.sleep(0.5) # 延时0.5s】
每次发送后等待对方确认接收数据,确认完毕后再发送下一条(加验证),否则重传
减少一次性发送和接收数据的大小,理论上buffer size越小丢包或粘包率就越低,建议在1024~10240之间
编程的时候,如果要跟某个IP建立连接,我们需要调用操作系统提供的 socket API。
socket 在操作系统层面,可以理解为一个文件。我们可以对这个文件进行一些方法操作。
用listen方法,可以让程序作为服务器监听其他客户端的连接。
用connect,可以作为客户端连接服务器。
用send或write可以发送数据,recv或read可以接收数据。
在建立好连接之后,如果我们想给远端服务发点什么东西,那就只需要对这个文件执行写操作就行了。
剩下的发送工作自然就是由操作系统内核来完成了。
既然是写给操作系统,那操作系统就需要提供一个地方给用户写。同理,接收消息也是一样。这个地方就是 socket 缓冲区。
用户发送消息的时候写给 send buffer(发送缓冲区)
用户接收消息的时候写给 recv buffer(接收缓冲区)
也就是说一个socket ,会带有两个缓冲区,一个用于发送,一个用于接收。因为这是个先进先出的结构,有时候也叫它们发送、接收队列。
在企业中开发的程序通信,消息往往有格式定义。消息的格式定义可以归入OSI中的表示层
比如:定义的消息,包括消息头和消息体。
消息头存放消息的数据格式(消息的长度、类型、状态等),消息体存放具体传送的数据。
对于TCP协议传输信息的程序来说,格式一定要有明确规定的消息边界。因为TCP传输的是字节流(bytes stream),如果消息中没有指定边界或者长度,接收方就不知道完整的消息从字节流的哪里开始,到哪里结束。
指定消息的边界有两种方式:
1.用特殊字节作为消息的结尾符号
可以用消息中不易出现的字符串(比如 FFFFFF)作为消息的结尾字符
2.在消息开头某个位置,直接指定消息的长度
UDP协议通常不用指定边界,因为UDP是数据报协议,应用程序从socket接收到的
必定是发送方发送的完整消息。