一、websocket概要:
websocket是基于TCP传输层协议实现的一种标准协议,用于在客户端和服务端双向传输数据
传统的客户端想要知道服务端处理进度有两个途径:
1)通过ajax不断轮询,由于http的无状态性,每次轮询服务器都需要去解析http协议,对服务器压力也很大
2)采用long poll的方式,服务端不给客户端反馈,客户端就一直等待,服务就一直被挂起,此阶段一直是阻塞状态
而当服务器完成升级(http–>websocket)后,上面两个问题就得到解决了:
1)被动性,升级后,服务端可以主动推送消息给客户端,解决了轮询造成的同步延迟问题
2)升级后,websocket只需要一次http握手,服务端就能一直与客户端保持通信,直到关闭连接,这样就解决了服务器需要反复解析http协议,减少了资源的开销
二、通过Flask创建websocket服务端
1 群聊服务端
from flask import Flask, request
from geventwebsocket.handler import WebSocketHandler
from gevent.pywsgi import WSGIServer
import json
app = Flask(__name__)
user_list=[]
@app.route("/")
def index():
#请求连接
user_socket = request.environ.get("wsgi.websocket") # type:WebSocket
user_list.append(user_socket)
print(len(user_list),user_list)
while 1:#循环接收消息
msg = user_socket.receive()
for user in user_list:
if user_socket != user:
user.send(msg)
if __name__ == '__main__':
http_serv = WSGIServer(("0.0.0.0", 5000), app, handler_class=WebSocketHandler) #将app实例化到WSGI中,然后运行服务器
http_serv.serve_forever()
1 群聊客户端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>群聊页面</h1>
<div id="d1" style="height: 500px;width: 800px;background: burlywood">
<p id="p1"></p>
</div>
<p>发送内容:<input type="text" id="message"><button onclick="send_msg()">发送消息</button></p>
</body>
</html>
<script type="text/javascript">
var ws = new WebSocket("ws://127.0.0.1:5000/");
ws.onmessage = function (res) {
console.log(res.data);
var ptag = document.createElement("p");
ptag.innerText = res.data;
document.getElementById("d1").appendChild(ptag);
};
function send_msg() {
var msg = document.getElementById("message").value;
var ptag = document.createElement("p");
ptag.style.cssText = "text-align: right;";
ptag.innerText = msg;
document.getElementById("d1").appendChild(ptag);
ws.send(msg);
}
</script>
用户1
用户2
2 单聊服务端
from flask import Flask, request, render_template
from geventwebsocket.handler import WebSocketHandler
from gevent.pywsgi import WSGIServer
import json
app = Flask(__name__)
user_dict={}
@app.route("/<username>")
def index(username):
print(username)#用户名
user_socket = request.environ.get("wsgi.websocket") # type:WebSocket
user_dict[username]=user_socket#将用户和对应的socket加入到字典中
print(len(user_dict),user_dict)
while 1: #循环接收消息
msg = user_socket.receive() #接收消息
msg_dict=json.loads(msg) #反序列化消息为字典
to=msg_dict.get("to_user")
message=msg_dict.get("msg")
user=user_dict.get(to)
user.send(message) #发送给要发送的人
if __name__ == '__main__':
http_serv = WSGIServer(("0.0.0.0", 5000), app, handler_class=WebSocketHandler)
http_serv.serve_forever()
2 单聊客户端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>群聊页面</h1>
<div id="d1" style="height: 500px;width: 800px;background: burlywood;">
<p id="p1"></p>
</div>
<p>您的昵称:<input type="text" id="name">
<button onclick="openws()">进入聊天室</button>
</p>
<p>给谁:<input type="text" id="to_user"></p>
<p>发送内容:<input type="text" id="message">
<button onclick="send_msg()">发送消息</button>
</p>
</body>
</html>
<script type="text/javascript">
var ws = null;
function openws() { //通过点击函数连接socket并初始化ws
var name = document.getElementById("name").value;
ws = new WebSocket("ws://127.0.0.1:5000/" + name);
ws.onmessage = function (res) {//接收到消息时触发此函数
console.log(res.data);
var ptag = document.createElement("p");//创建p标签
ptag.innerText = res.data;//为p标签设定值
document.getElementById("d1").appendChild(ptag); //在div的父标签下加入p标签
};
}
function send_msg() {//发送消息时触发此函数,但必须先让用户进入聊天室
var msg = document.getElementById("message").value;//获得消息
var from_user = document.getElementById("name").value;//注册用户名
var to_user = document.getElementById("to_user").value;//发送给谁
var ptag = document.createElement("p");//创建p标签
ptag.style.cssText = "text-align: right;";
ptag.innerText = msg;
document.getElementById("d1").appendChild(ptag);//在div下创建p标签,自己发送的消息在右侧
var msg_obj = {//实例化对象
msg: msg,
from_user: from_user,
to_user: to_user
};
console.log(msg_obj);
ws.send(JSON.stringify(msg_obj));//序列化成字符串
}
</script>
用户1
用户2
三、TCP 连接的“ 三次握手 ”与“ 四次挥手 ”
三次握手
握手之前主动打开连接的客户端结束CLOSED阶段,被动打开的服务器端也结束CLOSED阶段,并进入LISTEN阶段。随后开始“三次握手”:
(1)首先客户端向服务器端发送一段TCP报文,其中:
标记位为SYN,表示“请求建立新连接”;
序号为Seq=X(X一般为1);
随后客户端进入SYN-SENT阶段。
(2)服务器端接收到来自客户端的TCP报文之后,结束LISTEN阶段。并返回一段TCP报文,其中:
标志位为SYN和ACK,表示“确认客户端的报文Seq序号有效,服务器能正常接收客户端发送的数据,并同意创建新连接”(即告诉客户端,服务器收到了你的数据);
序号为Seq=y;
确认号为Ack=x+1,表示收到客户端的序号Seq并将其值加1作为自己确认号Ack的值;随后服务器端进入SYN-RCVD阶段。
(3)客户端接收到来自服务器端的确认收到数据的TCP报文之后,明确了从客户端到服务器的数据传输是正常的,结束SYN-SENT阶段。并返回最后一段TCP报文。其中:
标志位为ACK,表示“确认收到服务器端同意连接的信号”(即告诉服务器,我知道你收到我发的数据了);
序号为Seq=x+1,表示收到服务器端的确认号Ack,并将其值作为自己的序号值;
确认号为Ack=y+1,表示收到服务器端序号Seq,并将其值加1作为自己的确认号Ack的值;
随后客户端进入ESTABLISHED阶段。
服务器收到来自客户端的“确认收到服务器数据”的TCP报文之后,明确了从服务器到客户端的数据传输是正常的。结束SYN-SENT阶段,进入ESTABLISHED阶段。
在客户端与服务器端传输的TCP报文中,双方的确认号Ack和序号Seq的值,都是在彼此Ack和Seq值的基础上进行计算的,这样做保证了TCP报文传输的连贯性。一旦出现某一方发出的TCP报文丢失,便无法继续"握手",以此确保了"三次握手"的顺利完成。
此后客户端和服务器端进行正常的数据传输。这就是“三次握手”的过程。
“四次挥手”的详解
(1)首先客户端想要释放连接,向服务器端发送一段TCP报文,其中:
标记位为FIN,表示“请求释放连接“;
序号为Seq=U;
随后客户端进入FIN-WAIT-1阶段,即半关闭阶段。并且停止在客户端到服务器端方向上发送数据,但是客户端仍然能接收从服务器端传输过来的数据。
注意:这里不发送的是正常连接时传输的数据(非确认报文),而不是一切数据,所以客户端仍然能发送ACK确认报文。
(2)服务器端接收到从客户端发出的TCP报文之后,确认了客户端想要释放连接,随后服务器端结束ESTABLISHED阶段,进入CLOSE-WAIT阶段(半关闭状态)并返回一段TCP报文,其中:
标记位为ACK,表示“接收到客户端发送的释放连接的请求”;
序号为Seq=V;
确认号为Ack=U+1,表示是在收到客户端报文的基础上,将其序号Seq值加1作为本段报文确认号Ack的值;
随后服务器端开始准备释放服务器端到客户端方向上的连接。
客户端收到从服务器端发出的TCP报文之后,确认了服务器收到了客户端发出的释放连接请求,随后客户端结束FIN-WAIT-1阶段,进入FIN-WAIT-2阶段
前"两次挥手"既让服务器端知道了客户端想要释放连接,也让客户端知道了服务器端了解了自己想要释放连接的请求。于是,可以确认关闭客户端到服务器端方向上的连接了
(3)服务器端自从发出ACK确认报文之后,经过CLOSED-WAIT阶段,做好了释放服务器端到客户端方向上的连接准备,再次向客户端发出一段TCP报文,其中:
标记位为FIN,ACK,表示“已经准备好释放连接了”。注意:这里的ACK并不是确认收到服务器端报文的确认报文。
序号为Seq=W;
确认号为Ack=U+1;表示是在收到客户端报文的基础上,将其序号Seq值加1作为本段报文确认号Ack的值。
随后服务器端结束CLOSE-WAIT阶段,进入LAST-ACK阶段。并且停止在服务器端到客户端的方向上发送数据,但是服务器端仍然能够接收从客户端传输过来的数据。
(4)客户端收到从服务器端发出的TCP报文,确认了服务器端已做好释放连接的准备,结束FIN-WAIT-2阶段,进入TIME-WAIT阶段,并向服务器端发送一段报文,其中:
标记位为ACK,表示“接收到服务器准备好释放连接的信号”。
序号为Seq=U+1;表示是在收到了服务器端报文的基础上,将其确认号Ack值作为本段报文序号的值。
确认号为Ack=W+1;表示是在收到了服务器端报文的基础上,将其序号Seq值作为本段报文确认号的值。
随后客户端开始在TIME-WAIT阶段等待2MSL
举个栗子:把客户端比作男孩,服务器比作女孩。通过他们的分手来说明“四次挥手”过程。
“第一次挥手”:日久见人心,男孩发现女孩变成了自己讨厌的样子,忍无可忍,于是决定分手,随即写了一封信告诉女孩。
“第二次挥手”:女孩收到信之后,知道了男孩要和自己分手,怒火中烧,心中暗骂:你算什么东西,当初你可不是这个样子的!于是立马给男孩写了一封回信:分手就分手,给我点时间,我要把你的东西整理好,全部还给你!男孩收到女孩的第一封信之后,明白了女孩知道自己要和她分手。随后等待女孩把自己的东西收拾好。
“第三次挥手”:过了几天,女孩把男孩送的东西都整理好了,于是再次写信给男孩:你的东西我整理好了,快把它们拿走,从此你我恩断义绝!
“第四次挥手”:男孩收到女孩第二封信之后,知道了女孩收拾好东西了,可以正式分手了,于是再次写信告诉女孩:我知道了,这就去拿回来!