WebSocket
Wei Jieyang Lv4

简介

使用

WebSocket API 是 HTML5 标准的一部分, 但这并不代表 WebSocket 一定要用在 HTML 中,或者只能在基于浏览器的应用程序中使用。

实际上,许多语言、框架和服务器都提供了 WebSocket 支持:

  • 基于 C 的 libwebsocket.org
  • 基于 Node.js 的 Socket.io
  • 基于 Python 的 ws4py
  • 基于 C++ 的 WebSocket++
  • Apache 对 WebSocket 的支持: Apache Module mod_proxy_wstunnel
  • Nginx 对 WebSockets 的支持: NGINX as a WebSockets Proxy 、 NGINX Announces Support for WebSocket Protocol 、WebSocket proxying
  • lighttpd 对 WebSocket 的支持:mod_websocket

nodejs

java

Spring

服务端

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
package me.gacl.websocket;

import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;

/**
* @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,
* 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
*/
@ServerEndpoint("/websocket")
public class WebSocketTest {
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;

//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
private static CopyOnWriteArraySet<WebSocketTest> webSocketSet = new CopyOnWriteArraySet<WebSocketTest>();

//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;

/**
* 连接建立成功调用的方法
* @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
@OnOpen
public void onOpen(Session session){
this.session = session;
webSocketSet.add(this); //加入set中
addOnlineCount(); //在线数加1
System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());
}

/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(){
webSocketSet.remove(this); //从set中删除
subOnlineCount(); //在线数减1
System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
}

/**
* 收到客户端消息后调用的方法
* @param message 客户端发送过来的消息
* @param session 可选的参数
*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("来自客户端的消息:" + message);
//群发消息
for(WebSocketTest item: webSocketSet){
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
continue;
}
}
}

/**
* 发生错误时调用
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error){
System.out.println("发生错误");
error.printStackTrace();
}

/**
* 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。
* @param message
* @throws IOException
*/
public void sendMessage(String message) throws IOException{
this.session.getBasicRemote().sendText(message);
//this.session.getAsyncRemote().sendText(message);
}

public static synchronized int getOnlineCount() {
return onlineCount;
}

public static synchronized void addOnlineCount() {
WebSocketTest.onlineCount++;
}

public static synchronized void subOnlineCount() {
WebSocketTest.onlineCount--;
}
}

客户端

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
<%@ page language="java" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
<title>Java后端WebSocket的Tomcat实现</title>
</head>
<body>
Welcome<br/><input id="text" type="text"/>
<button onclick="send()">发送消息</button>
<hr/>
<button onclick="closeWebSocket()">关闭WebSocket连接</button>
<hr/>
<div id="message"></div>
</body>

<script type="text/javascript">
var websocket = null;
//判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:8080/websocket");
}
else {
alert('当前浏览器 Not support websocket')
}

//连接发生错误的回调方法
websocket.onerror = function () {
setMessageInnerHTML("WebSocket连接发生错误");
};

//连接成功建立的回调方法
websocket.onopen = function () {
setMessageInnerHTML("WebSocket连接成功");
}

//接收到消息的回调方法
websocket.onmessage = function (event) {
setMessageInnerHTML(event.data);
}

//连接关闭的回调方法
websocket.onclose = function () {
setMessageInnerHTML("WebSocket连接关闭");
}

//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
closeWebSocket();
}

//将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}

//关闭WebSocket连接
function closeWebSocket() {
websocket.close();
}

//发送消息
function send() {
var message = document.getElementById('text').value;
websocket.send(message);
}
</script>
</html>

Web

C++

c++下没有原生的websocket低矮用,但是可以使用一些开源库实现,如websocketpp等,下面讲解的是socket通信相关的实现

IOS

IOS网络通信

代码 - master分支

数据结构

sockaddrsockaddr_in在字节长度上都为16个BYTE,可以进行转换

1
2
3
4
5
/* 通用的socket地址 */
struct sockaddr{
unsigned short sa_family; //2
char sa_data[14]; //14
};
1
2
3
4
5
6
7
/* Internet socket */
struct sockaddr_in{
short int sin_family; //2
unsigned short int sin_port; //2
struct in_addr sin_addr; //4
unsigned char sin_zero[8]; //8
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 表示32位的IPv4地址 */
struct in_addr{
union {
struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { u_short s_w1,s_w2; } S_un_w;
u_long S_addr;
} S_un;
#define s_addr S_un.S_addr
};

/* 或者 */
struct in_addr{
in_addr_t s_addr;
};
  • **inet_addr(“192.168.0.1”)**:将一个点分制的IP地址(如192.168.0.1)转换为上述结构中需要的32位二进制方式的IP地址

函数

1
2
int socket(int domain, int type, int protocol);
int server_socket = socket(AF_INET, SOCK_STREAM, 0);
  • domain(IOS系统使用 AT_
    • PF_UNIX Unix IPC通信
    • PF_INET IPV4通信(默认)
    • PF_INET6 IPV6
    • PF_IPX Novell IPX
    • PF_NETLINK Kernel用户接口驱动程序
    • PF_X25 X.25
    • PF_AX25 AX.25
    • PF_ATMPVC ATM PVC
    • PF_APPLETALK AppleTalk协议
    • PF_PACKET 低级包接口
  • type
    • SOCK_STREAM 使用TCP面向连接的通信包(默认)
    • SOCK_DGRAM 使用UDP无连接的通信包
    • SOCK_SEQPACKET 使用有固定最大长度的面向连接的通信包
    • SOCK_RAW 使用原IP包
    • SOCK_RDM 使用不保证次序的可靠数据报
  • Protocol
    • 一般使用与type对应的默认协议,用0表示。
1
2
3
4
5
int recv(SOCKET socket, char *buf, int len, int flags);
char recv_msg[1024];
bzero(recv_msg, 1024);
long byte_num = recv(client_socket, recv_msg, 1024, 0);
recv_msg[byte_num] = '\0';
  • 参数:
    • socket:已建立连接的套接字;
    • buf:存放接收到的数据的缓冲区指针;
    • len:buf的长度
    • flags:调用方式:
      • 0:接收的是正常数据,无特殊行为。
      • MSG_PEEK:系统缓冲区数据复制到提供的接收缓冲区,但是系统缓冲区内容并没有删除。
      • MSG_OOB:表示处理带外数据。
  • 返回值:接收成功时返回接收到的数据长度,连接结束时返回0,连接失败时返回SOCKET_ERROR。
1
2
3
4
int send(SOCKET socket, const char *buf, int len, int flags)
char send_msg[1024];
bzero(send_msg, 1024);
send(client_socket, send_msg, 1024, 0);
  • 参数
    • socket:已建立连接的套接字
    • buf:存放将要发送的数据的缓冲区指针;
    • len:发送缓冲区中的字符数
    • flags:控制数据传输方式:
      • 0:接收的是正常数据,无特殊行为。
      • MSG_DONTROUTE:表示目标主机就在本地网络中,无需路由选择。
      • MSG_OOB:表示处理带外数据。
  • 返回值:发送成功时返回发送的数据长度,连接结束时返回0,连接失败时返回SOCKET_ERROR。
1
2
3
4
5
extern void bzero(void *s, int n);
char msg[1024];
bzero(msg, 1024);
/* 相当于 */
memset(msg, 0, 1024);
  • 头文件:#include <string.h>
  • 功能:置字节字符串s的前n个字节为零且包括‘\0’
  • 描述:
    • string.h曾经是posix标准的一部分,但是在POSIX.1-2001标准里面,这些函数被标记为了遗留函数而不推荐使用。在POSIX.1-2008标准里已经没有这些函数了。推荐使用memset替代bzero
    • bzero函数TC和VC中都没有,gcc中提供了
  • 无返回值

使用无阻塞的I/O方法

什么是阻塞?
比如使用recv(),如果函数接受不到数据,就会阻塞程序的继续执行。

如何防止阻塞?
使用fcntl()函数,把套接字设置为无阻塞模式。

1
2
3
>int newsocket; 
>newsocket = socket(PF_INET, SOCK_STREAM, 0 );
>fcntl( newsocket, F_SETEL, O_NONBLOCK );

以后使用recv()就不会阻塞了。

另一种方式是使用多路套接字select()

select

select这个系统调用,是一种多路复用IO方案,可以同时对多个文件描述符进行监控,从而知道哪些文件描述符(File Descriptor,FD)可读,可写或者出错,不过select方法是阻塞的,可以设定超时时间。

select使用的步骤如下:

  1. 创建一个fd_set变量(fd_set实为包含了一个整数数组的结构体),用来存放所有的待检查的文件描述符
  2. 清空fd_set变量,并将需要检查的所有文件描述符加入fd_set
  3. 调用select。若返回-1,则说明出错;返回0,则说明超时,返回正数,则为发生状态变化的文件描述符的个数
  4. 若select返回大于0,则依次查看哪些文件描述符变的可读,并对它们进行处理
  5. 返回步骤2,开始新一轮的检测
1
2
3
#include <sys/time.h> 
#include <unistd.h>
int select(int maxfd, fd_set *rdset, fd_set *wrset, fd_set *exset, struct timeval *timeout);
  • 参数:
    • maxfd:需要监视的最大的文件描述符值+1;
    • rdset:需要检测的可读文件描述符的集合
    • wrset:可写文件描述符的集合
    • exset:异常文件描述符的集合
    • timeval:用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。

fd_set类型通过下面四个宏来操作:

  1. FD_ZERO(fd_set *fdset);将指定的文件描述符集清空,在对文件描述符集合进行设置前,必须对其进行初始化,如果不清空,由于在系统分配内存空间后,通常并不作清空处理,所以结果是不可知的。
  2. FD_SET(fd_set *fdset);用于在文件描述符集合中增加一个新的文件描述符。
  3. FD_CLR(fd_set *fdset);用于在文件描述符集合中删除一个文件描述符。
  4. FD_ISSET(int fd, fd_set *fdset);用于测试指定的文件描述符是否在该集合中。
kqueue

Mac是基于BSD的内核,所使用的是kqueue(kernel event notification mechanism,详细内容可以Mac中 man 2 kqueue

  • kqueue比select先进的地方就在于使用事件触发的机制,且其调用无需每次对所有的文件描述符进行遍历,返回的时候只返回需要处理的事件,而不像select中需要自己去一个个通过FD_ISSET检查。
  • kqueue默认的触发方式是level 水平触发,可以通过设置event的flag为EV_CLEAR 使得这个事件变为边沿触发,可能epoll的触发方式无法细化到单个event,需要查证。

kqueue中涉及两个系统调用,kqueue()和kevent()

  • kqueue()创建kernel级别的事件队列,并返回队列的文件描述符
  • kevent()往事件队列中加入订阅事件,或者返回相关的事件数组

kqueue使用的流程一般如下:

  • 创建kqueue
  • 创建struct kevent变量(注意这里的kevent是结构体类型名),可以通过EV_SET这个宏提供的快捷方式进行创建
  • 通过kevent系统调用将创建好的kevent结构体变量加入到kqueue队列中,完成对指定文件描述符的事件的订阅
  • 通过kevent系统调用获取满足条件的事件队列,并对每一个事件进行处理

操作流程

截屏2021-01-31 下午2.57.07

1. sever

1
2
3
4
5
6
7
/* 绑定server socket的ip、端口等信息 */
struct sockaddr_in server_addr;
server_addr.sin_len = sizeof(struct sockaddr_in);
server_addr.sin_family = AF_INET;//Address families AF_INET互联网地址簇
server_addr.sin_port = htons(11332);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
bzero(&(server_addr.sin_zero),8);
  • 一般情况下都用server_addr.sin_addr.s_addr = htonl(INADDR_ANY)

    比如你的机器有三个ip
    192.168.1.1
    202.202.202.202
    61.1.2.3

    如果你serv.sin_addr.s_addr=inet_addr(“192.168.1.1”);

    然后监听100端口

    这时其他机器只有connect 192.168.1.1:100端口才能成功。
    connect 202.202.202.202:100和connect 61.1.2.3:100都会失败。

    如果serv.sin_addr.s_addr=htonl(INADDR_ANY); 的话,无论连接哪个ip都可以连上的,这就是为什么这样选择的理由

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
/* server socket工作流程 */
//创建socket
int server_socket = socket(AF_INET, SOCK_STREAM, 0);//SOCK_STREAM 有连接
if (server_socket == -1) {
perror("socket error");
return 1;
}

//绑定socket
//将创建的socket绑定到本地的IP地址和端口,此socket是半相关的,只是负责侦听客户端的连接请求,并不能用于和客户端通信
int bind_result = bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (bind_result == -1) {
perror("bind error");
return 1;
}

//listen侦听
//第一个参数是套接字
//第二个参数为等待接受的连接的队列的大小,在connect请求过来的时候,完成三次握手后先将连接放到这个队列中,直到被accept处理。如果这个队列满了,且有新的连接的时候,对方可能会收到出错信息。
if (listen(server_socket, 5) == -1) {
perror("listen error");
return 1;
}

//accept接收来自客户端的链接请求(使用下面的select不需要这一部分)
//返回的client_socket为一个全相关的socket,其中包含client的地址和端口信息,通过client_socket可以和客户端进行通信。
struct sockaddr_in client_address;
socklen_t address_len;
int client_socket = accept(server_socket, (struct sockaddr *)&client_address, &address_len);
if (client_socket == -1) {
perror("accept error");
return -1;
}
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
/* 信息交流 */
fd_set server_fd_set;
int max_fd = -1;
struct timeval tv;
tv.tv_sec = 20; // seconds
tv.tv_usec = 0; // microseconds
while (1) {
FD_ZERO(&server_fd_set);
//标准输入
FD_SET(STDIN_FILENO, &server_fd_set);
if (max_fd < STDIN_FILENO) {
max_fd = STDIN_FILENO;
}
//服务器端socket
FD_SET(server_sock_fd, &server_fd_set);
if (max_fd < server_sock_fd) {
max_fd = server_sock_fd;
}
//客户端连接
for (int i = 0; i < CONCURRENT_MAX; i++) {
if (client_fds[i]!=0) {
FD_SET(client_fds[i], &server_fd_set);

if (max_fd < client_fds[i]) {
max_fd = client_fds[i];
}
}
}
int ret = select(max_fd+1, &server_fd_set, NULL, NULL, &tv);
if (ret < 0) {
perror("select 出错\n");
continue;
}else if(ret == 0){
printf("select 超时\n");
continue;
}else{
//ret为未状态发生变化的文件描述符的个数
if (FD_ISSET(STDIN_FILENO, &server_fd_set)) {
//标准输入
bzero(input_msg, BUFFER_SIZE);
fgets(input_msg, BUFFER_SIZE, stdin);
//输入 ".quit" 则退出服务器
if (strcmp(input_msg, QUIT_CMD) == 0) {
exit(0);
}
for (int i=0; i<CONCURRENT_MAX; i++) {
if (client_fds[i]!=0) {
send(client_fds[i], input_msg, BUFFER_SIZE, 0);
}
}
}
if (FD_ISSET(server_sock_fd, &server_fd_set)) {
//有新的连接请求
struct sockaddr_in client_address;
socklen_t address_len;
int client_socket_fd = accept(server_sock_fd, (struct sockaddr *)&client_address, &address_len);
if (client_socket_fd > 0) {
int index = -1;
for (int i = 0; i < CONCURRENT_MAX; i++) {
if (client_fds[i] == 0) {
index = i;
client_fds[i] = client_socket_fd;
break;
}
}
if (index >= 0) {
printf("新客户端(%d)加入成功 %s:%d \n",index,inet_ntoa(client_address.sin_addr),ntohs(client_address.sin_port));
}else{
bzero(input_msg, BUFFER_SIZE);
strcpy(input_msg, "服务器加入的客户端数达到最大值,无法加入!\n");
send(client_socket_fd, input_msg, BUFFER_SIZE, 0);
printf("客户端连接数达到最大值,新客户端加入失败 %s:%d \n",inet_ntoa(client_address.sin_addr),ntohs(client_address.sin_port));
}
}
}
for (int i = 0; i <CONCURRENT_MAX; i++) {
if (client_fds[i]!=0) {
if (FD_ISSET(client_fds[i], &server_fd_set)) {
//处理某个客户端过来的消息
bzero(recv_msg, BUFFER_SIZE);
long byte_num = recv(client_fds[i],recv_msg,BUFFER_SIZE,0);
if (byte_num > 0) {
if (byte_num > BUFFER_SIZE) {
byte_num = BUFFER_SIZE;
}
recv_msg[byte_num] = '\0';
printf("客户端(%d):%s\n",i,recv_msg);
}else if(byte_num < 0){
printf("从客户端(%d)接受消息出错.\n",i);
}else{
FD_CLR(client_fds[i], &server_fd_set);
client_fds[i] = 0;
printf("客户端(%d)退出了\n",i);
}
}
}
}
}
}

2. client

1
2
3
4
5
6
7
/* 创建需要通信的server socket的IP、端口等信息 */
struct sockaddr_in server_addr;
server_addr.sin_len = sizeof(struct sockaddr_in);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(11332);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
bzero(&(server_addr.sin_zero),8);
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
/* 创建socket通信连接 */
//创建socket
int client_socket = socket(AF_INET, SOCK_STREAM, 0);
if (client_socket == -1) {
perror("socket error");
return 1;
}

//连接client和server的通信道路
int connect_result = connect(client_socket, (struct sockaddr *)&server_addr, sizeof(struct sockaddr_in));

//connect 成功之后,其实系统将你创建的socket绑定到一个系统分配的端口上,且其为全相关,包含服务器端的信息,可以用来和服务器端进行通信。
char recv_msg[1024];
char reply_msg[1024];
if (connect_result == 0){
while (1) {
bzero(recv_msg, 1024);
bzero(reply_msg, 1024);
long byte_num = recv(client_socket, recv_msg, 1024, 0);
recv_msg[byte_num] = '\0';
printf("server said:%s\n",recv_msg);

printf("client reply:");
scanf("%s",reply_msg);
if (send(client_socket, reply_msg, 1024, 0) == -1) {
perror("send error");
}
}
}

WIN

模仿Unix Socket技术实现

操作流程

截屏2021-01-31 下午2.57.07

php

  • Post title:WebSocket
  • Post author:Wei Jieyang
  • Create time:2021-01-25 14:37:08
  • Post link:https://jieyang-wei.github.io/2021/01/25/WebSocket/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.