websocket

发布于:2024-05-09 ⋅ 阅读:(34) ⋅ 点赞:(0)
1、概念

是一种在单个TCP连接上进行全双工通信的协议。websocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

  • 单工通信:数据传输只允许在一个方向上传输,只能一方发送数据,另一方接收数据并发送。
  • 半双工:数据传输允许两个方向上的传输,但在同一时间内,只可以有一方发送或接收数据。
  • 全双工:同时可进行双向数据传输。
2、背景

客户端发送请求,服务器响应后即断开连接,HTTP 协议无法实现服务器主动向客户端发起消息,因此无法支持实时性要求高的应用场景,比如在线游戏、即时聊天、股票行情等。

一般都是使用轮询技术,轮询是在特定的时间间隔,由浏览器对客户端发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然后HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的宽带等资源。

在这种情况下,HTML5定义了websocket协议,能更好的节省服务器资源和宽带,而且能够更实时地进行通讯。

轮询技术:

短轮询:setInterval,每隔一段时间客户端向服务端请求一次,一次请求就是一次http请求,都会伴随一次tcp连接,即三次握手,四次挥手。这样的弊端一个是会产生大量无意义的请求。还有就是频繁地打开关闭连接,还有实时性并不高。

长轮询:客户端向服务端发起请求,如果没有消息服务端就不响应,等有了消息在响应,然后客户端再次请求。这样的话请求都变得有意义了。弊端:客户端长时间得不到响应会导致超时,从而主动断开和服务器的连接(可以解决:ajax若因超时而结束,立即重新发送请求到服务器)

3、实现原理

WebSocket 协议(还是基于tcp协议,所以还会有三次握手,四次挥手),在三次握手和四次挥手之间会有一个websocket握手的过程,在这个websocket握手之后,服务器可以任意的给客户端发消息,客户端也可以随意给服务器发消息。工作原理如下:

  1. 握手阶段(Handshake):(先得发个http请求(固定以ws开头,还会有wss),但是是告诉服务器我们之后通信就不用http了,用websocket了)

    • 客户端发起一个普通的 HTTP 或 HTTPS 请求到服务器,请求包含一些特殊的头部信息,如 Upgrade: websocket ,Connection: Upgrade,Sec-Websocket-Version: 13,Sec-Websocket-Key:(随机生成,会变化)
    • 服务器收到请求后,如果支持 WebSocket,会返回一个 HTTP 101 状态码(切换协议),表示协议升级成功,升级成websocket,同时响应中也包含一些特殊的头部信息,如 Upgrade: websocket , Connection: Upgrade,Sec-Websocket-Accept:(对应那个key生成的)
    • 客户端和服务器之间的连接升级为 WebSocket 连接,之后所有的通信都是基于 WebSocket 协议进行。
  2. 数据传输阶段(Data Transfer)

    • 一旦 WebSocket 连接建立成功,客户端和服务器之间可以随时相互发送数据。
    • 数据以帧(Frame)的形式进行传输,每个帧包含一个或多个数据块,可以是文本或二进制数据。
    • 客户端和服务器可以在任何时候发送数据,并且可以同时发送和接收数据,实现了真正的全双工通信。
  3. 连接关闭阶段(Connection Close)

    • 当客户端或服务器决定关闭连接时,它们可以发送一个关闭帧来结束连接。
    • 收到关闭帧的一方会响应一个关闭帧,然后双方都关闭连接。
4、优点

1)创建连接后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。

2)由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少。

缺点:

1)兼容性,h5才有

2)维持tcp连接需要耗费资源,消息比较少,那维持这个连接比较浪费资源

5、与HTTP相比

相同:

WebSocket和HTTP都是基于TCP/IP协议栈的应用层协议。

WebSocket和HTTP都是用于客户端和服务器之间的通信

不同:

1)通信方式:

HTTP:HTTP是一种单向的、无状态的通信协议,半双工。通常是客户端向服务器发起请求,服务器返回响应的模式。
WebSocket:WebSocket是一种全双工的通信协议,允许客户端和服务器之间同时发送和接收数据,实现了双向通信。
2)连接方式:

HTTP:HTTP是一种短连接的协议,每次通信都需要建立一个新的连接,通信结束后立即关闭连接。
WebSocket:WebSocket是一种长连接的协议,一旦建立连接,客户端和服务器之间可以保持长时间的通信,不需要频繁地建立和断开连接。
3)数据格式:

HTTP:HTTP通常使用文本格式来传输数据,如HTML、JSON等。
WebSocket:WebSocket支持传输任意格式的数据,可以是文本、二进制等。
4)实时性和效率:

HTTP:HTTP通常是基于请求-响应模式,因此在实时性和效率上有一定的限制,适用于普通的网页浏览和数据传输。
WebSocket:WebSocket是专门设计用于实时通信的协议,实时性和效率较高,适用于需要实时通信或推送消息的场景。
5)适用场景:

HTTP:适用于普通的网页浏览、数据传输等场景。
WebSocket:适用于实时聊天应用、在线游戏、实时数据展示等需要实时通信的场景

6、例子

1)使用原生websocket API

新建项目

新建两个文件夹

server

        打开终端,执行npm init -y

        安装npm i ws

        新建server.js

        

        终端执行node server.js

project

        

import React, { useState } from 'react';

const App = () => {
    const [serverUrl, setServerUrl] = useState('');
    const [messageInput, setMessageInput] = useState('');
    const [messageLog, setMessageLog] = useState('');
    const [ws, setWs] = useState();
    const [imageInput, setImageInput] = useState(null);
    const [imagePreview, setImagePreview] = useState(null);

/**
 * onopen	连接建立时触发
  onmessage	客户端接收服务端数据时触发
  onerror	通信发生错误时触发
  onclose	连接关闭时触发
  onbeforeunload	当浏览器窗口关闭或者刷新时触发
 */
    const connect = () => {
        const nws = new WebSocket(serverUrl);
        setWs(nws);
        nws.onopen = () => {
            logMessage('客户端连接服务端');
        };
        nws.onmessage = (event) => {
            logMessage(`客户端接受消息: ${event.data}`);
        };
        nws.onclose = () => {
            logMessage('Connection closed');
        };
        nws.onerror = (error) => {
            logMessage(`WebSocket error: ${error}`);
        };
    };

    const sendMessage = () => {
      console.log(ws);
        if (ws && ws.readyState === WebSocket.OPEN) {
            ws.send(messageInput);
            setMessageInput('');
            logMessage(`Sent: ${messageInput}`);
        } else {
            logMessage('WebSocket connection not open');
        }
    };

    const logMessage = (message) => {
        setMessageLog(prevLog => prevLog + message + '\n');
    };

    const sendMessageImage = () => {
//ws.readyState 0-正在连接中 1-已连接 2-正在关闭中 3-已关闭
      if (ws && ws.readyState === WebSocket.OPEN) {
          if (imageInput) {
              const reader = new FileReader();
              reader.onload = (event) => {
                console.log(event.target);
                  ws.send(event.target.result);
                  setImagePreview(event.target.result);
                logMessage(`Sent: ${event.target.result}`);
              };
              reader.readAsDataURL(imageInput);
          } else {
              setMessageLog((prevLog) => prevLog + 'Please select an image\n');
          }
      } else {
          setMessageLog((prevLog) => prevLog + 'WebSocket connection not established\n');
      }
  };

  const handleImageChange = (e) => {
      const file = e.target.files[0];
      setImageInput(file);
  };

    return (
        <div>
            <h1>WebSocket Test</h1>
            
            <div>
                <label htmlFor="serverUrl">WebSocket Server URL:</label>
                <input
                    type="text"
                    id="serverUrl"
                    placeholder="ws://example.com:8080"
                    value={serverUrl}
                    onChange={(e) => setServerUrl(e.target.value)}
                />
                <button onClick={connect}>Connect</button>
            </div>
            
            <div>
                客户<textarea
                    id="messageInput"
                    placeholder="Type a message..."
                    value={messageInput}
                    onChange={(e) => setMessageInput(e.target.value)}
                ></textarea>
                <button onClick={sendMessage}>Send Message</button>
            </div>
            <input
                type="file"
                id="imageInput"
                onChange={handleImageChange}
            />
            <button onClick={sendMessageImage}>Send Image</button>
            <div>
                <pre>{messageLog}</pre>
                {imagePreview && <img src={imagePreview} alt="Image Preview" style={{ maxWidth: '100px' }} />}
            </div>
        </div>
    );
};

export default App;

运行该项目

有一些问题:链接双方可以任何时候发送任何消息,不知道消息的含义是啥

在http中有path,固定那个接口获取的就是哪个数据

解决这个问题就是使用第三方库socket.io

2)socket.io

客户端和服务器双方事先约定好不同的事件,事件由谁监听,由谁触发,就可以有序管理消息了

const express = require('express');
const http = require('http');
const app = express();
const server = http.createServer(app);
const io = require('socket.io')(server, {
    cors: {
      origin: "http://localhost:3001",
      methods: ["GET", "POST"]
    }
  });

// 用于存储当前在线用户的信息
const onlineUsers = {};

// 监听客户端连接事件
io.on('connection', (socket) => {
  console.log('有新的连接');

  // 监听客户端发送的用户名事件
  socket.on('add user', (username) => {
    // 将用户名与对应的 socket.id 存储起来
    onlineUsers[socket.id] = username;

    // 向所有客户端广播新用户加入的消息
    io.emit('user joined', { username: username, numUsers: Object.keys(onlineUsers).length });

    console.log(username + ' 加入了聊天室');
  });

  // 监听客户端发送的消息事件
  socket.on('chat message', (msg) => {
    console.log('收到消息:', msg);
    
    // 将消息发送给所有连接的客户端(广播)
    io.emit('chat message', { username: onlineUsers[socket.id], message: msg });
  });

  // 监听客户端断开连接事件
  socket.on('disconnect', () => {
    // 如果用户在加入聊天室后断开连接,则发送断开连接消息
    if (onlineUsers[socket.id]) {
      const username = onlineUsers[socket.id];
      delete onlineUsers[socket.id];

      // 向所有客户端广播用户离开的消息
      io.emit('user left', { username: username, numUsers: Object.keys(onlineUsers).length });

      console.log(username + ' 离开了聊天室');
    }
  });
});

// 服务启动
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});
import React, { useState, useEffect } from 'react';
import io from 'socket.io-client';

const App = () => {
  const [socket, setSocket] = useState(null);
  const [username, setUsername] = useState('');
  const [message, setMessage] = useState('');
  const [messages, setMessages] = useState([]);
  const [numUsers, setNumUsers] = useState(0);

  useEffect(() => {
    // 连接到服务器
    const newSocket = io('http://localhost:3000');
    setSocket(newSocket);

    // 当组件卸载时断开连接
    return () => newSocket.disconnect();
  }, []);

  // 监听新用户加入的消息
  useEffect(() => {
    if (socket) {
      socket.on('user joined', ({ username, numUsers }) => {
        setNumUsers(numUsers);
        addMessage(`${username} 加入了聊天室`);
      });
    }
  }, [socket]);

  // 监听用户离开的消息
  useEffect(() => {
    if (socket) {
      socket.on('user left', ({ username, numUsers }) => {
        setNumUsers(numUsers);
        addMessage(`${username} 离开了聊天室`);
      });
    }
  }, [socket]);

  // 添加一条新消息到消息列表
  const addMessage = (msg) => {
    console.log(msg);
    setMessages(prevMessages => [...prevMessages, msg]);
  };

  // 发送消息
  const sendMessage = () => {
    if (message.trim() !== '') {
      socket.emit('chat message', message);
      setMessage('');
    }
    socket.emit('chat message', message);
      setMessage('');
  };

  // 加入聊天室
  const joinChatRoom = () => {
    if (username.trim() !== '') {
      socket.emit('add user', username);
    }
  };

  return (
    <div>
      <h1>聊天室</h1>
      <div>当前在线用户: {numUsers}</div>
      <div>
        {messages.map((msg, index) => {
            console.log(msg);
            return (
            <div key={index}>{msg}</div>
            )})}
      </div>
      <div>
        <input type="text" value={username} onChange={(e) => setUsername(e.target.value)} placeholder="输入用户名" />
        <button onClick={joinChatRoom}>加入聊天室</button>
      </div>
      <div>
        <input type="text" value={message} onChange={(e) => setMessage(e.target.value)} placeholder="输入消息" />
        <button onClick={sendMessage}>发送消息</button>
      </div>
    </div>
  );
};

export default App;