Post

Socket

Socket

Socket(套接字)是计算机网络通信中一个重要的概念,它是一种用于描述 IP 地址和端口的组合,用于标识网络上的通信双方,实现不同计算机之间的双向通信。以下是详细介绍:

分类

  • 流式套接字(SOCK_STREAM) :提供面向连接的、可靠的、双向的字节流通信。使用 TCP(传输控制协议)作为传输协议,在正式通信前,通信双方需要先建立连接,数据传输时保证数据的完整性和顺序性。例如,当我们访问一个网站,浏览器通过流式套接字与网站服务器建立连接,确保网页数据正确无误地传输到浏览器端,完整展示网页内容。
  • 数据报套接字(SOCK_DGRAM) :提供无连接的、不可靠的、基于消息的数据报服务。使用 UDP(用户数据报协议)作为传输协议,不保证数据传输的可靠性,数据以独立的数据报形式发送,每个数据报都有其目标地址和端口号。它适用于对实时性要求较高但对数据丢失不太敏感的应用场景,如视频直播、在线游戏中的实时消息传递等。

原理

  • 在工作传输层,提供网络通信接口
    • Socket 位于 OSI(开放系统互连)模型的传输层,主要为应用层提供网络通信的接口。它屏蔽了底层网络通信的复杂细节,使得应用程序开发者无需关注网络协议的具体实现,只需通过 Socket 提供的接口进行数据的发送和接收。
    • 例如,当开发一个聊天软件时,开发者不需要了解 TCP/IP 协议栈的具体工作机制,只需调用 Socket 接口,将消息通过 Socket 发送给对方,或者从 Socket 接收对方的消息。
  • 与 IP 地址和端口号绑定
    • Socket 与 IP 地址和端口号紧密相关。每个 Socket 都有一个唯一的 IP 地址和端口号的组合,用于标识网络上的一个特定的通信端点。当数据在网络上传输时,会先到达目标主机的 IP 地址,然后根据端口号找到对应的 Socket,从而将数据传递给相应的应用程序进程。
    • 以一个服务器为例,服务器会将自己提供服务的 Socket 绑定到一个特定的端口号上,例如 HTTP 服务通常绑定在 80 端口。当客户端向服务器发送 HTTP 请求时,数据包会先发送到服务器的 IP 地址,然后服务器根据端口号 80 将数据转发给处理 HTTP 服务的 Socket 所关联的应用程序。
  • 基于内核缓冲区实现数据传输
    • Socket 的数据传输基于内核缓冲区。当应用程序通过 Socket 发送数据时,数据会先被写入到内核缓冲区,然后由操作系统负责将缓冲区中的数据通过网络协议栈发送出去。同样,当接收数据时,数据先从网络接口进入内核缓冲区,应用程序再从缓冲区中读取数据。
    • 这个机制可以保证数据的高效传输,因为应用程序与内核缓冲区之间的数据传输速度相对较快,而内核缓冲区与网络协议栈之间的数据传输则由操作系统进行优化和调度。例如,在一个高速网络环境下,应用程序可以不断地将数据写入内核缓冲区,而操作系统会根据网络带宽和对方接收能力等因素,合理地将缓冲区中的数据分批发送到网络上。

一个简单的服务器解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#!/usr/bin/env python
# encoding: utf-8

import sys
import os
import base64
import hashlib
import socket

sys.path.append(os.path.abspath(os.path.dirname(__file__) + '/' + '..'))
sys.path.append("..")

sock = socket.socket()//启动一个socket
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)//设定socket选项这里用的是ipv4和TCP
# 绑定host, 默认端口5000
sock.bind(("192.168.1.161", 9900))
sock.listen(5)//进入被动模式阻塞监听表示 等待连接的最大队列长度即最多允许 5 个未接受的连接处于等待状态超过的连接会被拒绝客户端会收到 connection refused 错误)。


def get_headers(data):
    """
    将请求头转换为字典
    :param data: 解析请求头的data
    :return: 请求头字典
    """
    header_dict = {}
    data = str(data, encoding="utf-8")
    header, body = data.split("\r\n\r\n", 1)  # 因为请求头信息结尾都是\r\n,并且最后末尾部分是\r\n\r\n; 所以以此分割请求头和请求体
    header_list = header.split("\r\n")
    for i in range(0, len(header_list)):
        if i == 0:
            if len(header_list[0].split(" ")) == 3:
                header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[0].split(" ")//请求方式请求地址和具体的http协议
        else:
            k, v = header_list[i].split(":", 1)
            header_dict[k] = v.strip()
    return header_dict


def decode_info(info):
    """
    对返回消息进行解码
    :param info: 原始消息
    :return: 解码之后的汉字
    """
    if len(info) < 1:
        return "no data"
    payload_len = info[1] & 127
    if payload_len == 126:
        extend_payload_len = info[2:4]
        mask = info[4:8]
        decoded = info[8:]
    elif payload_len == 127:
        extend_payload_len = info[2:10]
        mask = info[10:14]
        decoded = info[14:]
    else:
        extend_payload_len = None
        mask = info[2:6]
        decoded = info[6:]
    bytes_list = bytearray()  # 使用字节将数据全部收集,再去字符串编码,这样不会导致中文乱码
    for i in range(len(decoded)):
        chunk = decoded[i] ^ mask[i % 4]  # 解码方式
        bytes_list.append(chunk)
    body = str(bytes_list, encoding='utf-8')
    return body


# 等待用户连接
conn, addr = sock.accept()
print("conn from==>", conn, addr)
# 获取握手消息, magic string,sha1加密
# 发送给客户端
# 握手消息
data = conn.recv(8096)//一次最大读入8096个字节
headers = get_headers(data)

# 对请求头中的sec - websocket - key进行加密
response_tpl = "HTTP/1.1 101 Switching Protocols\r\n" \
               "Upgrade:websocket\r\n" \
               "Connection: Upgrade\r\n" \
               "Sec-WebSocket-Accept: %s\r\n" \
               "WebSocket-Location: ws://%s%s\r\n\r\n"
"""
第一行101状态码,表示握手成功
第二三行表示将http协议升格到websocket
第四行是要返回发过来的由客户端的 Sec-WebSocket-Key 和固定 magic_string 计算得到(magic_string是在websocket协议中固定下来的,这一步是避免和其他服务器造成影响)
第五行是可选字段,指定 WebSocket 服务的 URL(非必需,但代码中包含以兼容部分客户端)
"""
magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
# 确认握手Sec - WebSocket - Key固定格式: headers头部的Sec - WebSocket - Key+'258EAFA5-E914-47DA-95CA-C5AB0DC85
value = headers['Sec-WebSocket-Key'] + magic_string
ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())
response_str = response_tpl % (ac.decode('utf-8'), headers['Host'], headers['url'])//SHA1加密后用base64编码填充到响应头之中

# 响应【握手】信息
conn.send(bytes(response_str, encoding='utf-8'))//响应

# 可以进行通信-接收客户端发送的消息
while True:
    data = conn.recv(8096)
    data = decode_info(data)
    print("Receive msg==>", data)
    if data == "no data":
        conn.close()
  • websocket的握手主要在osi七层模型中的应用层,而TCP的三次握手在传输层
  • 客户端向服务器发送一个带有 SYN 标志位的初始连接请求报文,包含随机生成的初始序列号 x。
  • 服务器收到请求后,如果同意建立连接,会发送一个带有 SYN 和 ACK 标志位的确认报文,其中包含服务器的初始序列号 y,同时将确认序列号设置为 x+1。
  • 客户端收到服务器的确认报文后,会发送一个带有 ACK 标志位的确认报文,将确认序列号设置为 y+1,服务器收到该确认报文后,连接建立成功。

一个使用原生websocket的客户端实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport"
          content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"/>
    <title>DiyAbc智能小车控制</title>
    <style type="text/css">

        .controller {
            float: left;
            width: 49%;
        }

            .controller p {
                text-align:center;
                font-size: 18px;
            }

        .controller table tr td{
            padding: 10px;
            text-align: center;
        }

    </style>
</head>
<body>
    <h3>DiyAbc智能小车控制</h3>
    <div id="login">
        <div style="margin: 10px;">
            <input id="serverIP" type="text" placeholder="服务器IP" value="192.168.137.37" autofocus="autofocus" />
            <input id="serverPort" type="text" placeholder="服务器端口" value="9900" />
            <input id="btnConnect" type="button" value="连接" onclick="connect()" />
        </div>
        <div style="margin: 10px;">
            <input id="sendText" type="text" placeholder="发送文本" value="up" />
            <input id="btnSend" type="button" value="发送" onclick="send()" />
        </div>

        <div style="margin: 10px;">
            <img id="imgCamera" src="" style="width: 100%" />
        </div>


        <div>

            <div class="controller" id="car" >
                <p>车控制</p>
                <table >
                    <tr>
                        <td colspan="3">
                            <button onclick="sendCmd('up')">前进</button>
                        </td>

                    </tr>
                    <tr>
                        <td> <button onclick="sendCmd('left')">左转</button></td>
                        <td> <button onclick="sendCmd('stop')">停止</button></td>
                        <td> <button onclick="sendCmd('right')">右转</button></td>
                    </tr>
                    <tr>
                        <td colspan="3">
                            <button onclick="sendCmd('back')">后退</button>
                        </td>
                    </tr>
                </table>
            </div>


            <div class="controller" id="camera" >
                <p>摄像头</p>
                <table >
                    <tr>
                        <td colspan="3">
                            <button onclick="sendCmd('cam_up')">上转</button>
                        </td>

                    </tr>
                    <tr>
                        <td> <button onclick="sendCmd('cam_left')">左转</button></td>
                        <td> <button onclick="sendCmd('cam_init')">归位</button></td>
                        <td> <button onclick="sendCmd('cam_right')">右转</button></td>
                    </tr>
                    <tr>
                        <td colspan="3">
                            <button onclick="sendCmd('cam_down')">下转</button>
                        </td>
                    </tr>
                </table>
            </div>


        </div>

    </div>
    <script type="text/javascript" src="./js/jquery.min.js"></script>
    <script type="text/javascript" src="./js/layer/layer.js"></script>

    <script type="text/javascript">
        var socket;
        var isConnect=false;
        //http://192.168.137.37:8080/?action=stream
        function cameraConntct() {
            var host= "http://" + $("serverIP").value + ":8080/?action=stream"
           document.getElementById("imgCamera").setAttribute("src",host);
        }//视频这一块用的是http协议,用的是mjpg-streamer这个项目,暂时按下不表
        
        function connect() {

            var host = "ws://" + $("serverIP").value + ":" + $("serverPort").value + "/"
            socket = new WebSocket(host);//创建一个新的socket实例并绑定
            try {

                socket.onopen = function (msg) {
                    $("btnConnect").disabled = true;
                    isConnect=true;
                    cameraConntct();
                    layer.msg("连接成功!");
                };//接受成功

                socket.onmessage = function (msg) {
                    if (typeof msg.data == "string") {
                        displayContent(msg.data);
                    }
                    else {
                        layer.msg("非文本消息");
                    }
                };//接受到信息

                socket.onclose = function (msg) {
                    $("btnConnect").disabled = false;
                    isConnect=false;
                    layer.msg("连接已断开!")
                };//连接断开
            }
            catch (ex) {
                log(ex);
            }
        }

        function send() {

            if(!isConnect){
                layer.msg("请先连接智能小车");
                return false;
            }

            var msg = $("sendText").value
            socket.send(msg);
        }//读取自定义命令并发送

        function sendCmd(cmd){
            if(!isConnect){
                layer.msg("请先连接智能小车");
                return false;
            }
            socket.send(cmd);
        }

        window.onbeforeunload = function () {
            try {
                socket.close();
                socket = null;
            }
            catch (ex) {
            }
        };//处理关闭事件

        function $(id) { return document.getElementById(id); }
        function onkey(event) { if (event.keyCode == 13) { send(); } }
    </script>
</body>
</html>
This post is licensed under CC BY 4.0 by the author.