分类 Ubuntu 下的文章

“Ubuntu是以桌面应用为主的Linux发行版,基于Debian。Ubuntu有三个正式版本,包括桌面版、服务器版及用于物联网设备和机器人的Core版。从17.10版本开始,Ubuntu以GNOME为默认桌面环境。 Ubuntu是著名的Linux发行版之一,也是目前最多用户的Linux版本。 ”

如果一个连接服务器不断开,则curl_easy_perform会一直阻塞。这个时候就需要关闭或断开连接的方法。

常用的方法是设置连接超时,或者总超时,本处使用原始socket close

CURLINFO_ACTIVESOCKET - get the active socket

Example

CURL *curl = curl_easy_init();
if(curl) {
  curl_socket_t sockfd;
  curl_easy_setopt(curl, CURLOPT_URL, "https://example.com");
 
  /* Do not do the transfer - only connect to host */
  curl_easy_setopt(curl, CURLOPT_CONNECT_ONLY, 1L);
  res = curl_easy_perform(curl);
 
  /* Extract the socket from the curl handle */
  res = curl_easy_getinfo(curl, CURLINFO_ACTIVESOCKET, &sockfd);
 
  if(res != CURLE_OK) {
    printf("Error: %s\n", curl_easy_strerror(res));
    return 1;
  }
}

另一个获取sock的方法

curl_easy_setopt(curl, CURLOPT_OPENSOCKETFUNCTION, my_opensocketfunc);

curl_socket_t my_opensocketfunc(void *clientp,curlsocktype purpose,struct curl_sockaddr *address)
{
    curl_socket_t sock=socket(address->family, address->socktype, address->protocol);
    printf("my_opensocketfunc sock = %d\n", sock);
    return  sock;
}

超时的配置见
CURLOPT_CONNECTTIMEOUT

Timeout for the connection phase. See CURLOPT_CONNECTTIMEOUT

CURLOPT_CONNECTTIMEOUT_MS

Millisecond timeout for the connection phase. See CURLOPT_CONNECTTIMEOUT_MS

CURLOPT_TIMEOUT

Timeout for the entire request. See CURLOPT_TIMEOUT

Linux system函数返回值判断

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
 
int main()
{
    pid_t status;
 
 
    status = system("./test.sh");
 
    if (-1 == status)
    {
        printf("system error!");
    }
    else
    {
        printf("exit status value = [0x%x]\n", status);
 
        if (WIFEXITED(status))
        {
            if (0 == WEXITSTATUS(status))
            {
                printf("run shell script successfully.\n");
            }
            else
            {
                printf("run shell script fail, script exit code: %d\n", WEXITSTATUS(status));
            }
        }
        else
        {
            printf("exit status = [%d]\n", WEXITSTATUS(status));
        }
    }
 
    return 0;
}

Referenced from:https://blog.csdn.net/cheyo/article/details/6595955

C语言中的popen()函数

#include <stdlib.h>  
#include <stdio.h>  
  
#define BUF_SIZE 1024  
char buf[BUF_SIZE];  
  
int main(void)  
{  
    FILE * p_file = NULL;  
  
    p_file = popen("ifconfig eth0", "r");  
    if (!p_file) {  
        fprintf(stderr, "Erro to popen");  
    }  
  
    while (fgets(buf, BUF_SIZE, p_file) != NULL) {  
        fprintf(stdout, "%s", buf);  
    }  
    pclose(p_file);     
  
    return 0;  
}  

输出结果:
Referenced from:https://blog.csdn.net/yzy1103203312/article/details/78483566

spdlog 下载编译

wget https://github.com/gabime/spdlog/archive/refs/tags/v1.9.2.tar.gz
tar xvf v1.9.2.tar.gz
cd spdlog-1.9.2/
cmake -DCMAKE_INSTALL_PREFIX=$(pwd)/../../install .

spdlog 简单使用

#include "spdlog/spdlog.h"
#include "spdlog/sinks/basic_file_sink.h"

int main() 
{
    spdlog::info("Welcome to spdlog!");
    spdlog::error("Some error message with arg: {}", 1);
    
    spdlog::warn("Easy padding in numbers like {:08d}", 12);
    spdlog::critical("Support for int: {0:d};  hex: {0:x};  oct: {0:o}; bin: {0:b}", 42);
    spdlog::info("Support for floats {:03.2f}", 1.23456);
    spdlog::info("Positional args are {1} {0}..", "too", "supported");
    spdlog::info("{:<30}", "left aligned");
    
    spdlog::set_level(spdlog::level::debug); // Set global log level to debug
    spdlog::debug("This message should be displayed..");    
    
    // change log pattern
    spdlog::set_pattern("[%H:%M:%S %z] [%n] [%^---%L---%$] [thread %t] %v");
    
    // Compile time log levels
    // define SPDLOG_ACTIVE_LEVEL to desired level
    SPDLOG_TRACE("Some trace message with param {}", 42);
    SPDLOG_DEBUG("Some debug message");
    
    // Set the default logger to file logger
    auto file_logger = spdlog::basic_logger_mt("basic_logger", "logs/basic.txt");
    spdlog::set_default_logger(file_logger);            
}

spdlog 常见问题

//打印行号
// 先设置日志输出格式
// %s:文件名,my_file.cpp
// %#:行号,123
// %!:函数名,my_func
spdlog::set_pattern("%Y-%m-%d %H:%M:%S [%l] [%t] - <%s>|<%#>|<%!>,%v");

// 使用宏才会有行号
SPDLOG_DEBUG("Some debug message");
spdlog::info("Welcome to spdlog!");

志输出格式具体见:https://github.com/gabime/spdlog/wiki/3.-Custom-formatting

spdlog 推荐用法

#define DEBUG(...) SPDLOG_LOGGER_DEBUG(spdlog::default_logger_raw(), __VA_ARGS__)
#define LOG(...) SPDLOG_LOGGER_INFO(spdlog::default_logger_raw(), __VA_ARGS__)
#define WARN(...) SPDLOG_LOGGER_WARN(spdlog::default_logger_raw(), __VA_ARGS__)
#define ERROR(...) SPDLOG_LOGGER_ERROR(spdlog::default_logger_raw(), __VA_ARGS__)

DEBUG("debug");
LOG("info");
WARN("warn");
ERROR("error");

spdlog 控制台输出

// 设置默认logger为控制台即可
// 设置默认logger,这里是控制台,所以spdlog::info的内容会输出到控制台
auto console = spdlog::stdout_color_mt("console");
spdlog::set_default_logger(console);

spdlog 控制台输出官方代码

#include "spdlog/spdlog.h"
#include "spdlog/sinks/stdout_color_sinks.h"
void stdout_example()
{
    // create color multi threaded logger
    auto console = spdlog::stdout_color_mt("console");    
    //auto err_logger = spdlog::stderr_color_mt("stderr");    
    spdlog::get("console")->info("loggers can be retrieved from a global registry using the spdlog::get(logger_name)");
}

spdlog 同时输出控制台和写日志文件

// spdlog 同时输出console和文件
#define DEBUG(...) SPDLOG_LOGGER_DEBUG(spdlog::default_logger_raw(), __VA_ARGS__);SPDLOG_LOGGER_DEBUG(spdlog::get("daily_logger"), __VA_ARGS__)
#define LOG(...) SPDLOG_LOGGER_INFO(spdlog::default_logger_raw(), __VA_ARGS__);SPDLOG_LOGGER_INFO(spdlog::get("daily_logger"), __VA_ARGS__)
#define WARN(...) SPDLOG_LOGGER_WARN(spdlog::default_logger_raw(), __VA_ARGS__);SPDLOG_LOGGER_WARN(spdlog::get("daily_logger"), __VA_ARGS__)
#define ERROR(...) SPDLOG_LOGGER_ERROR(spdlog::default_logger_raw(), __VA_ARGS__);SPDLOG_LOGGER_ERROR(spdlog::get("daily_logger"), __VA_ARGS__)

auto console = spdlog::stdout_color_mt("console");
spdlog::set_default_logger(console);
// 每天2:30 am 新建一个日志文件
auto logger = spdlog::daily_logger_mt("daily_logger", "logs/daily.txt", 2, 30);
// 遇到warn flush日志,防止丢失
logger->flush_on(spdlog::level::warn);

spdlog 文件按天分割

#include "spdlog/sinks/daily_file_sink.h"
void daily_example()
{
    // Create a daily logger - a new file is created every day on 2:30am
    auto logger = spdlog::daily_logger_mt("daily_logger", "logs/daily.txt", 2, 30);
}

spdlog 定时flush到文件
spdlog为了提高性能,降低对磁盘的写操作,通过flush机制来一次性把日志写入到文件里面持久化。所以如果没有恰当的配置,停止调试或者进程崩溃的时候会有日志丢失的问题。

//每三秒刷新一次
spdlog::flush_every(std::chrono::seconds(3));

遇到error级别,立即flush到文件:

enum level_enum
{
    trace = SPDLOG_LEVEL_TRACE, // 最低
    debug = SPDLOG_LEVEL_DEBUG,
    info = SPDLOG_LEVEL_INFO,
    warn = SPDLOG_LEVEL_WARN,
    err = SPDLOG_LEVEL_ERROR,
    critical = SPDLOG_LEVEL_CRITICAL, // 最高
    off = SPDLOG_LEVEL_OFF,
    n_levels
};

auto logger = spdlog::daily_logger_mt("daily_logger", "log/daily.txt", 2, 30);
// 遇到warn或者更高级别,比如err,critical 立即flush日志,防止丢失
logger->flush_on(spdlog::level::warn);

spdlog 使用完整代码

#include "spdlog/spdlog.h"
#include "spdlog/sinks/rotating_file_sink.h"
#include "spdlog/sinks/daily_file_sink.h"
#include "spdlog/sinks/stdout_color_sinks.h"
#include <iostream>
#include <memory>

// spd 带行号的打印,同时输出console和文件
#define DEBUG(...) SPDLOG_LOGGER_DEBUG(spdlog::default_logger_raw(), __VA_ARGS__);SPDLOG_LOGGER_DEBUG(spdlog::get("daily_logger"), __VA_ARGS__)
#define LOG(...) SPDLOG_LOGGER_INFO(spdlog::default_logger_raw(), __VA_ARGS__);SPDLOG_LOGGER_INFO(spdlog::get("daily_logger"), __VA_ARGS__)
#define WARN(...) SPDLOG_LOGGER_WARN(spdlog::default_logger_raw(), __VA_ARGS__);SPDLOG_LOGGER_WARN(spdlog::get("daily_logger"), __VA_ARGS__)
#define ERROR(...) SPDLOG_LOGGER_ERROR(spdlog::default_logger_raw(), __VA_ARGS__);SPDLOG_LOGGER_ERROR(spdlog::get("daily_logger"), __VA_ARGS__)

int main(int argc, char *argv[]) {
    // 按文件大小
    //auto file_logger = spdlog::rotating_logger_mt("file_log", "log/log.log", 1024 * 1024 * 100, 3);
    // 每天2:30 am 新建一个日志文件
    auto logger = spdlog::daily_logger_mt("daily_logger", "logs/daily.txt", 2, 30);
    // 遇到warn flush日志,防止丢失
    logger->flush_on(spdlog::level::warn);
    //每三秒刷新一次
    spdlog::flush_every(std::chrono::seconds(3));
    
    // Set the default logger to file logger
    auto console = spdlog::stdout_color_mt("console");
    spdlog::set_default_logger(console);
    spdlog::set_level(spdlog::level::debug); // Set global log level to debug

    // change log pattern
    // %s:文件名
    // %#:行号
    // %!:函数名
    spdlog::set_pattern("%Y-%m-%d %H:%M:%S [%l] [%t] - <%s>|<%#>|<%!>,%v");

    LOG("test info");
    ERROR("test error");
    
    // Release and close all loggers
    spdlog::drop_all();
}

spdlog 线程安全
对于sinks,以 _mt 后缀结尾的是线程安全的,比如:daily_file_sink_mt
以_st 后缀结尾的是非线程安全的,比如:daily_file_sink_st

spdlog 接口
spdlog 总体而言提供了日志接口

spdlog::debug(), 默认的日志对象,使用默认的日志信息格式,输出至 stdout。
logger->debug(), 指定日志对象进行日志记录,输出至该日志对象对应的文件中。
SPDLOG_LOGGER_DEBUG(logger), SPDLOG_DEBUG(), 使用宏对以上两种接口进行包装,产生的日志格式包含 文件、函数、行。

spdlog set_level 设置日志级别

低于设置级别的日志将不会被输出。各level排序,数值越大级别越高:

// Runtime log levels
spd::set_level(spd::level::info); //Set global log level to info
console->debug("This message should not be displayed!");
console->set_level(spd::level::debug); // Set specific logger's log level
console->debug("This message should be displayed..");

第一行日志debug级别低于设定的级别info,在level为info时不会被输出。
第二行日志debug级别与设定的级别相同,所以可以显示出来。

typedef enum
{
    trace = 0,
    debug = 1,
    info = 2,
    warn = 3,
    err = 4,
    critical = 5,
    off = 6
} level_enum;

spdlog 同步和异步设置
Asynchronous logging

官方说明:https://github.com/gabime/spdlog/wiki/6.-Asynchronous-logging

默认情况下是不开启异步模式的,开启异步模式方式如下:

size_t q_size = 4096; //queue size must be power of 2
spdlog::set_async_mode(q_size);

队列大小:

队列占用的内存 = 设置的队列大小 * slot的大小, 64位系统下slot大小为104字节。由此可根据系统的log输出总量来确定队列大小。

spdlog drop -- 释放logger

在程序结束时,应该调用drop_all() 释放所有logger。

There is a bug in VS runtime that cause the application dead lock when it exits. If you use async logging, please make sure to call spdlog::drop_all() before main() exit. If some loggers are not in the registry, those should be released manually as well. stackoverflow: std::thread join hangs if called after main exits when using vs2012 rc

// Release and close all loggers
spdlog::drop_all();

或者单独drop某个logger

spd::drop("console");
spd::drop("basic_logger");

spdlog dump_backtrace

// 日志的所有信息能够被储存在一个环形缓冲里(包括 调试/追踪信息),之后可以根据需要将之打印出来。
// 需要时,调用dump_backtrace()

spdlog::enable_backtrace(32); // 保存最后的32条信息,早些时候的信息会被丢掉。
spdlog::dump_backtrace(); // 这时会打印出来,最后的32条信息。

spdlog::logger::dump_backtrace() will not work inside signal handlers.

spdlog_level environment variable

./example SPDLOG_LEVEL=info,mylogger=trace 

也可以使用

export SPDLOG_LEVEL=info
./example

#include "spdlog/cfg/env.h"
void load_levels_example()
{
    // Set the log level to "info" and mylogger to to "trace":
    // SPDLOG_LEVEL=info,mylogger=trace && ./example
    spdlog::cfg::load_env_levels();
    // or from command line:
    // ./example SPDLOG_LEVEL=info,mylogger=trace
    // #include "spdlog/cfg/argv.h" // for loading levels from argv
    // spdlog::cfg::load_argv_levels(args, argv);
}

websocket 维基百科介绍
WebSocket是一种网络传输协议,可在单个TCP连接上进行全双工通信,位于OSI模型的应用层。WebSocket协议在2011年由IETF标准化为RFC 6455「https://tools.ietf.org/html/rfc6455」,后由RFC 7936「https://tools.ietf.org/html/rfc7936」补充规范。

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

WebSocket是一种与HTTP不同的协议。两者都位于OSI模型的应用层,并且都依赖于传输层的TCP协议。 虽然它们不同,但是RFC 6455中规定:it is designed to work over HTTP ports 80 and 443 as well as to support HTTP proxies and intermediaries(WebSocket通过HTTP端口80和443进行工作,并支持HTTP代理和中介),从而使其与HTTP协议兼容。 为了实现兼容性,WebSocket握手使用HTTP Upgrade头[1]从HTTP协议更改为WebSocket协议。

WebSocket协议支持Web浏览器(或其他客户端应用程序)与Web服务器之间的交互,具有较低的开销,便于实现客户端与服务器的实时数据传输。 服务器可以通过标准化的方式来实现,而无需客户端首先请求内容,并允许消息在保持连接打开的同时来回传递。通过这种方式,可以在客户端和服务器之间进行双向持续对话。 通信通过TCP端口80或443完成,这在防火墙阻止非Web网络连接的环境下是有益的。另外,Comet之类的技术以非标准化的方式实现了类似的双向通信。

大多数浏览器都支持该协议,包括Google Chrome、Firefox、Safari、Microsoft Edge、Internet Explorer和Opera。

与HTTP不同,WebSocket提供全双工通信。此外,WebSocket还可以在TCP之上实现消息流。TCP单独处理字节流,没有固有的消息概念。 在WebSocket之前,使用Comet可以实现全双工通信。但是Comet存在TCP握手和HTTP头的开销,因此对于小消息来说效率很低。WebSocket协议旨在解决这些问题。

WebSocket协议规范将ws(WebSocket)和wss(WebSocket Secure)定义为两个新的统一资源标识符(URI)方案,分别对应明文和加密连接。除了方案名称和片段ID(不支持#)之外,其余的URI组件都被定义为此URI的通用语法。

websocket 背景
早期,很多网站为了实现推送技术,所用的技术都是轮询。轮询是指由浏览器每隔一段时间(如每秒)向服务器发出HTTP请求,然后服务器返回最新的数据给客户端。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求与回复可能会包含较长的头部,其中真正有效的数据可能只是很小的一部分,所以这样会消耗很多带宽资源。

比较新的轮询技术是Comet。这种技术虽然可以实现双向通信,但仍然需要反复发出请求。而且在Comet中普遍采用的HTTP长连接也会消耗服务器资源。

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

Websocket使用ws或wss的统一资源标志符(URI)。其中wss表示使用了TLS的Websocket。如:

ws://example.com/wsapi
wss://secure.example.com/wsapi
Websocket与HTTP和HTTPS使用相同的TCP端口,可以绕过大多数防火墙的限制。默认情况下,Websocket协议使用80端口;运行在TLS之上时,默认使用443端口。

websocket 优点

  • 较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。
  • 更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
  • 保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
  • 更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
  • 可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。
  • 更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。

websocket 请求应答示例
一个典型的Websocket握手请求如下:

客户端请求:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
服务器回应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

websocket 字段说明

  • Connection必须设置Upgrade,表示客户端希望连接升级。
  • Upgrade字段必须设置Websocket,表示希望升级到Websocket协议。
  • Sec-WebSocket-Key是随机的字符串,服务器端会用这些数据来构造出一个SHA-1的信息摘要。把“Sec-WebSocket-Key”加上一个特殊字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算SHA-1摘要,之后进行Base64编码,将结果做为“Sec-WebSocket-Accept”头的值,返回给客户端。如此操作,可以尽量避免普通HTTP请求被误认为Websocket协议。
  • Sec-WebSocket-Version 表示支持的Websocket版本。RFC6455要求使用的版本是13,之前草案的版本均应当弃用。
  • Origin字段是必须的。如果缺少origin字段,WebSocket服务器需要回复HTTP 403 状态码(禁止访问)。
  • 其他一些定义在HTTP协议中的字段,如Cookie等,也可以在Websocket中使用。

websocket 数据协议分析
协议官方文档地址:https://datatracker.ietf.org/doc/html/rfc6455#section-5.2

  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Extended payload length continued, if payload len == 127  |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |                               |Masking-key, if MASK set to 1  |
 +-------------------------------+-------------------------------+
 | Masking-key (continued)       |          Payload Data         |
 +-------------------------------- - - - - - - - - - - - - - - - +
 :                     Payload Data continued ...                :
 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |                     Payload Data continued ...                |
 +---------------------------------------------------------------+

FIN 位,也是整个片段的第一个字节的最高位,他只能是 0 或者是 1,这个位的作用只有一个,如果它为 1,表示这个片段是整个消息的最后一个片段,如果是 0,表示这个片段之后,还有其它的片段。RSV1,RSV2,RSV3
这三位是保留给扩展使用的,基本不会用到,反正我没用到,所以我们可以把它们当做空气就行,永远设置为 0,就是这么果断。

websocket opcode
opcode 顾名思义就是操作码,占用第一个字节的低四位,所以 opcode 可以代表 16 种不同的值。你是不是想问,opcode 是用来干嘛的?
opcode 是用 来解析当前片段的载荷(携带的数据)的,具体的后面会再次说明。

0x00,表示当前片段是连续片段,这是啥意思呢?还记得上面讨论 FIN 的时候,一条消息被分割成多条片段?如果当前片段不是第一个,那么 opcode 必须设置为 0。
0x01,表示当前片段所携带的数据是文本数据(记得最开始说的文本数据和二进制数据的区别??),如果有多个片段的话,只需要在第一个片段设置该值,属于同一条消息中后面的片段,只需要设置为 0 即可。
0x02,表示当前片段所携带的数据是二进制数据,如果有多个片段的话,只需要在第一个片段设置该值,属于同一条消息中后面的片段,只需要设置为 0 即可。
0x03-0x07,保留给将来使用,也就是说暂时还没用到。
0x08,表示关闭 websocket 连接,这个后面我会再一次讲到,先放着
0x09,发送 Ping 片段,说白了,它主要是用来检测远程端点是否还存活,我想检查我的对象是不是已经死了,但是这个片段可以携带数据,如果端点的一方发送了 Ping,那么接受方,必须返回 Pong 片段,用中国人的话来说,就是礼尚往来嘛。
0xA,发送 Pong,用以回复 Ping,是不是很简单?
0xB-F,保留给将来使用,也就是说暂时还没用到。
websocket MASK
表示当前片段所携带的数据是否经过加密,位置为第二个字节的最高位,总共 1 位,它的值不是你想设置就设置的啊,RFC6455 明确规定,所有从客户端发送给服务器的数据必须加密,所以 mask 的值必须是 1。还有,所有从服务器发往客户端的数据,一定不能加密,所以呢,mask 必须为 0,就是这么简单粗暴。

websocket Payload Length
这部分是用来定义负载数据的长度的,总共 7 位,所以最大值为 127,就这么简单?哼哼,不会的。
websocket 大于125字节 0x7e
payload_length<=125,此时数据的长度就是 payload_length 的大小。
payload_length=126,那么紧接着 payload_length 的 2 个字节,就用来表示数据的大小,所以当数据大小大于 125,小于 65535 的时候,payload_length 设置为 126,后面分析代码的时候,我会再次讲到。
payload_length=127,也就是 payload_length 取最大值,那么紧接着 payload_length 的 8 个字节,就用来表示数据的大小,此可以表示的数据可就相当大了,后面分析代码的时候,我会再次讲到。
websocket Mask key
它的位置紧接着数据长度的后面,大小为 0 或者是 4 个字节。前面分析了 mask 的作用,如果 mask 为 1 的话,数据需要加密,此时 mask key 占用 4 个字节,否则长度为 0,至于 mask key 如何用来解密数据的,后面会再次讲到。

websocket payload data
这里就是我们从客户端接收到的数据,不过它是经过加密的,“我是奥巴马”,之前 payload_length 的长度,就是经过加密之后的数据的长度,而不是原始数据的长度。
websocket 解析数据包
读取第二个字节的低 7 位,也就是之前讨论的 payload_length,0x7f 转换为二进制就是 01111111,当 payload_length 的长度小于 125 的话,数据长度就等于片段长度。当 payload_length 的长度等于 126 的时候,就有些麻烦了,此时第 3 和第 4 个字节组合为一个无符号 16 位整数,还记得我们之前说的,网络字节序吗?高位字节在前,低位字节在后面,所以当我们读的时候,第 3 个字节就是高 8 位,第 4 个字节就是低 8 位,所以我们首先将高 8 位左移 8 位再和低 8 位做或运算。当 payload_length 的长度等于 127 的时候,此时的第 3 到第 10 位组合为一个无符号 64 位整数,所以最高的 8 位需要左移 56 位,后面的依次类推,低 8 位保持不动。

websocket 解析 mask key
要找到 maskey,首先必须找到它在当前片段的偏移,如果 payload_length<=125,那么偏移就是 2,如果 payload_length==126,那么偏移就是 (2+2)=4,如果 payload_length>126,那么偏移就是(2+8)=10,同时 mask key 的大小为 4 个字节,所以找到了偏移和长度,mask key 就可以获取到了。

websocket 解密数据
解密数据的第一步就是要找到加密数据在当前片段中的偏移,很简单,这个值等于 maskkey 的偏移(上面已经求过了)+maskkey 本身的长度 4,那么怎么来解密数据呢?看上面的代码,就可以看出来,解密的过程其实就是遍历加密数据的每一个字符的 ASCII 值和数据(当前遍历的位置对 4 取模,得出的数据必定是 0,1,2,3,将得出的数据找到 maskkey 对应位置的 ASCII 值)进行异或运算求得,这个算法是 RFC6455 规定的,全世界都是这样。

websocket 掩码算法
掩码键(Masking-key)是由客户端挑选出来的32位的随机数。掩码操作不会影响数据载荷的长度。掩码、反掩码操作都采用如下算法:

首先,假设:

original-octet-i:为原始数据的第i字节。
transformed-octet-i:为转换后的数据的第i字节。
j:为i mod 4的结果。
masking-key-octet-j:为mask key第j字节。
算法描述为: original-octet-i 与 masking-key-octet-j 异或后,得到 transformed-octet-i。

j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j

websocket Sec-WebSocket-Accept的计算

Sec-WebSocket-Accept根据客户端请求首部的Sec-WebSocket-Key计算出来。

计算公式为:

将Sec-WebSocket-Key跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接。
通过SHA1计算出摘要,并转成base64字符串。
伪代码如下:

>toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 )  )
验证下前面的返回结果:

const crypto = require('crypto');
const magic = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const secWebSocketKey = 'w4v7O6xFTi36lq3RNcgctw==';

let secWebSocketAccept = crypto.createHash('sha1')
    .update(secWebSocketKey + magic)
    .digest('base64');

console.log(secWebSocketAccept);
// Oy4NRAQ13jhfONC7bP8dTKb4PTU=

websocket 接保持+心跳
WebSocket为了保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道保持连接没有断开。然而,对于长时间没有数据往来的连接,如果依旧长时间保持着,可能会浪费包括的连接资源。

但不排除有些场景,客户端、服务端虽然长时间没有数据往来,但仍需要保持连接。这个时候,可以采用心跳来实现。

发送方->接收方:ping
接收方->发送方:pong
ping、pong的操作,对应的是WebSocket的两个控制帧,opcode分别是0x9、0xA。

举例,WebSocket服务端向客户端发送ping,只需要如下代码(采用ws模块)

ws.ping('', false, true);

websocket 数据掩码的作用
WebSocket协议中,数据掩码的作用是增强协议的安全性。但数据掩码并不是为了保护数据本身,因为算法本身是公开的,运算也不复杂。除了加密通道本身,似乎没有太多有效的保护通信安全的办法。

那么为什么还要引入掩码计算呢,除了增加计算机器的运算量外似乎并没有太多的收益(这也是不少同学疑惑的点)。

答案还是两个字:安全。但并不是为了防止数据泄密,而是为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)等问题。

from:https://www.cnblogs.com/chyingp/p/websocket-deep-in.html

websocket test websocket server

ws://echo.websocket.org/

js 使用websocket

在支持WebSocket的浏览器中,在创建socket之后。可以通过onopen,onmessage,onclose即onerror四个事件实现对socket进行响应
一个简单是示例:

var ws = new WebSocket(“ws://localhost:8080”);
ws.onopen = function()
{
  console.log(“open”);
  ws.send(“hello”);
};
ws.onmessage = function(evt)  {  console.log(evt.data); };
ws.onclose   = function(evt)  {  console.log(“WebSocketClosed!”); };
ws.onerror   = function(evt)  {  console.log(“WebSocketError!”); };

首先申请一个WebSocket对象,参数是需要连接的服务器端的地址,同http协议使用http://开头一样,WebSocket协议的URL使用ws://开头,另外安全的WebSocket协议使用wss://开头。

websocket 0x81 0x7e 0x7f

注意,从客户端发送到服务端的数据都被 异或加密(用一个32位的key)格式化。详情请参见规范的第5节。掩码明确告知我们消息是否经过格式化。从客户端来的消息必须经过格式化,所以你的服务器必须要求这个掩码是1(事实上,规范5.1节规定了如果客户端发送了没有格式化的消息,你的服务器应该断开连接)

当向客户端发送帧时,不要对其进行掩码,也不要设置掩码位。稍后我们将解释屏蔽。注意:即使使用安全套接字,也必须屏蔽消息。RSV1-3可以忽略,它们是用于扩展的。

操作码字段定义了如何解释有效负载数据:0x0表示延续,0x1表示文本(总是用UTF-8编码),0x2表示二进制,以及其他所谓的“控制代码”,稍后将对此进行讨论。在这个版本的WebSockets中,0x3到0x7和0xB到0xF没有任何意义。

FIN位告诉我们这是不是系列的最后一条消息。如果是0,那么服务器将继续侦听消息的更多部分;否则,服务器应该考虑传递的消息。不仅仅是这样。

解码有效载荷长度
要读取有效负载数据,您必须知道何时停止读取。这就是为什么有效载荷长度很重要。不幸的是,这有点复杂。要阅读它,请遵循以下步骤:

读取9-15(包括)位并将其解析为无符号整型。如果长度小于等于125,那么就是长度;你就完成了。如果是126,到第二步。如果是127,到步骤3。
读取下面的16位,并将其解释为无符号整型。你就完成了。
读取接下来的64位,并将其解释为无符号整型(最重要的位必须为0)。
from: https://developer.mozilla.org/zh-CN/docs/Web/API/WebSockets_API/Writing_WebSocket_servers

websocket java代码实现

public static final String RESPONSE_HEADERS = "HTTP/1.1 101 Switching Protocols\r\n" +
            "Upgrade: websocket\r\n" +
            "Connection: Upgrade\r\n" +
            "WebSocket-Location: ws://127.0.0.1:9527\r\n";

 ServerSocket serverSocket = new ServerSocket(7000);

        while (true) {
            Socket socket = serverSocket.accept();
            // 开启一个新线程
            Thread thread = new Thread(() -> {
                // 响应握手信息
                try {
                    // 读取请求头
                    byte[] bytes = new byte[10000000];
                    socket.getInputStream().read(bytes);
                    String requestHeaders = new String(bytes, StandardCharsets.UTF_8);

                    // 获取请求头中的
                    String webSocketKey = "";
                    for (String header : requestHeaders.split("\r\n")) {
                        if (header.startsWith("Sec-WebSocket-Key")) {
                            webSocketKey = header.split(":")[1].trim();
                        }
                    }

                    // 将webSocketKey 与 magicKey 拼接用sha1加密之后在进行base64编码
                    String value = webSocketKey + magicKey;
                    String webSocketAccept = new String(Base64.encodeBase64(DigestUtils.sha1(value.getBytes(StandardCharsets.UTF_8))), StandardCharsets.UTF_8);

                    // 写入返回头 握手结束 成功建立连接
                    String responseHeaders = RESPONSE_HEADERS + "Sec-WebSocket-Accept: " + webSocketAccept + "\r\n\r\n";
                    socket.getOutputStream().write(responseHeaders.getBytes(StandardCharsets.UTF_8));
                    System.out.println("握手成功,成功建立连接");
                }
            }
        }

from: https://segmentfault.com/a/1190000039890327

websocket debugging-websockets-with-curl

$ curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Host: echo.websocket.org" -H "Origin:http://www.websocket.org" http://echo.websocket.org

HTTP/1.1 101 Web Socket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
WebSocket-Origin: http://www.websocket.org
WebSocket-Location: ws://echo.websocket.org/
Server: Kaazing Gateway
Date: Mon, 11 Jun 2012 16:34:46 GMT
Access-Control-Allow-Origin: http://www.websocket.org
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: content-type
Access-Control-Allow-Headers: authorization
Access-Control-Allow-Headers: x-websocket-extensions
Access-Control-Allow-Headers: x-websocket-version
Access-Control-Allow-Headers: x-websocket-protocol

Referenced from:https://www.thenerdary.net/post/24889968081/debugging-websockets-with-curl

websocket Base Framing Protocol
https://datatracker.ietf.org/doc/html/rfc6455#section-5.2

websocket implementing-web-sockets-with-libcurl

#define concat(a,b) a b
  handle = curl_easy_init();
  // Add headers
  header_list_ptr = curl_slist_append(NULL , "HTTP/1.1 101 WebSocket Protocol Handshake");
  header_list_ptr = curl_slist_append(header_list_ptr , "Upgrade: WebSocket");
  header_list_ptr = curl_slist_append(header_list_ptr , "Connection: Upgrade");
  header_list_ptr = curl_slist_append(header_list_ptr , "Sec-WebSocket-Version: 13");
  header_list_ptr = curl_slist_append(header_list_ptr , "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==");
  curl_easy_setopt(handle, CURLOPT_URL, concat("http","://echo.websocket.org"));
  curl_easy_setopt(handle, CURLOPT_HTTPHEADER, header_list_ptr);
  curl_easy_setopt(handle, CURLOPT_OPENSOCKETFUNCTION, my_opensocketfunc);
  curl_easy_setopt(handle, CURLOPT_HEADERFUNCTION, my_func);
  curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, my_writefunc);
  curl_easy_perform(handle);

Referenced from:https://phpandmore.net/2015/02/17/implementing-web-sockets-with-curl/

websocket 使用curl测试websocket服务

curl --include \
     --no-buffer \
     --header "Connection: Upgrade" \
     --header "Upgrade: websocket" \
     --header "Host: example.com:80" \
     --header "Origin: http://example.com:80" \
     --header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
     --header "Sec-WebSocket-Version: 13" \
     http://example.com:80/

Referenced from:https://blog.csdn.net/sd2131512/article/details/74996577

Test a WebSocket using curl.

curl \
    --include \
    --no-buffer \
    --header "Connection: Upgrade" \
    --header "Upgrade: websocket" \
    --header "Host: example.com:80" \
    --header "Origin: http://example.com:80" \
    --header "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
    --header "Sec-WebSocket-Version: 13" \
    http://example.com:80/

Referenced from:https://gist.github.com/htp/fbce19069187ec1cc486b594104f01d0