如何开发一个移动端信令网络库

1、背景介绍

最近几年一直在做IM相关的开发,对于IM通道侧接收消息的方式一般有三种:

  • 长连接推
  • 短连HTTP轮询拉
  • 推拉结合

IM通道有两个基本的要求:"不丢不重",纯推不丢不重机制复杂,纯拉性能和实时性又不是特别好,所以我们采用的是推拉结合的方案。当有新消息时长连接负责推送指令告知客户端有新消息,客户端再通过HTTP去请求最新消息。

这个通道和机制一直稳定运行了好多年,直到今年ChatGPT的到来,带来了很多业务场景,对长连接的通道的要求有高了起来。

2、建设长连接通道会有哪些问题

2.1 问题一 连接超时问题

面试时经常被问到的TCP三次握手过程,我们都能脱口而出了。我们也知道三次握手分别对应服务端的accept和客户端的connect,下面摘录了一个客户端服务端实现socket通信的demo:

server.c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main() {
    int sockfd, newsockfd, portno;
    socklen_t clilen;
    char buffer[256];
    struct sockaddr_in serv_addr, cli_addr;
    int n;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("ERROR opening socket");
        exit(1);
    }

    bzero((char *) &serv_addr, sizeof(serv_addr));
    portno = 5001;

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(portno);

    if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) {
        perror("ERROR on binding");
        exit(1);
    }

    listen(sockfd, 5);
    clilen = sizeof(cli_addr);
    printf("new client\n");

    newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
    printf("newsocketfd:%d\n", newsockfd);
    if (newsockfd < 0) {
        perror("ERROR on accept");
        exit(1);
    }

    bzero(buffer, 256);
    n = read(newsockfd, buffer, 255);
    if (n < 0) {
        perror("ERROR reading from socket");
        exit(1);
    }
    printf("Here is the message: %s\n", buffer);

    n = write(newsockfd, "I got your message", 18);
    if (n < 0) {
        perror("ERROR writing to socket");
        exit(1);
    }

    close(newsockfd);
    close(sockfd);
    return 0;
}
client.c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>

int main() {
    int sockfd, portno, n;
    struct sockaddr_in serv_addr;
    struct hostent *server;

    char buffer[256];
    portno = 5001;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("ERROR opening socket");
        exit(1);
    }

    server = gethostbyname("localhost");
    if (server == NULL) {
        fprintf(stderr, "ERROR, no such host\n");
        exit(0);
    }

    bzero((char *) &serv_addr, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    bcopy((char *)server->h_addr, (char *)&serv_addr.sin_addr.s_addr, server->h_length);
    serv_addr.sin_port = htons(portno);

    if (connect(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) {
        perror("ERROR connecting");
        exit(1);
    }

    printf("Please enter the message: ");
    bzero(buffer, 256);
    fgets(buffer, 255, stdin);

    n = write(sockfd, buffer, strlen(buffer));
    if (n < 0) {
        perror("ERROR writing to socket");
        exit(1);
    }

    bzero(buffer, 256);
    n = read(sockfd, buffer, 255);
    if (n < 0) {
        perror("ERROR reading from socket");
        exit(1);
    }
    printf("%s\n", buffer);
    close(sockfd);
    return 0;
}

运行demo时没有任何问题,因为demo一般都运行在我们相对稳定的环境。到了现实中更复杂的场景,可能客户端去connect时会超时或者失败,但是它并不是立即超时失败,在Linux 实现中,如果主动 connect 方没有收到 SYN 的回应,会在第6秒发送第2个 SYN 进行重试,第3个 SYN 则是与第2个间隔24秒。直到第75秒还没有收到回应,则 connect 调用返回 ETIMEOUT。

再具体点,什么时候回出现这种情况呢?我们列举一下常见场景:

  1. 网络彻底断了:直接返回失败;
  2. 服务端的accet函数所在线程阻塞(如上面例子,已经有一个client连接,并且在线程里被阻塞):客户端还是可以成长connect成功,建立连接时系统侧干的;
  3. 服务端负载达到极限或者中间路由问题:会连接很长时间失败
  4. 网络差,丢包严重:会连接很长时间失败

2.2 断开感知

socket断开靠读写失败来感知。要监听socket连接是否被断开,一般使用以下方法:

  1. 使用心跳包(Keep-Alive):你在一定时间间隔内,服务端和客户端相互发送小型的数据包以确认连接是否仍然有效。如果一方停止收到心跳包,就可以认为连接已经断开。
  2. 使用超时机制:在进行读取或写入操作时,你可以设置一个超时时间。如果在超时时间内没有收到数据,则可以认为连接已经断开。
  3. 通过错误码检测:在进行读取或写入操作后,检查返回的错误码。如果错误码指示连接已经断开,你可以进行相应的处理。
  4. 使用select()或poll()函数:你可以使用select()或poll()函数来检测套接字上的读取事件。如果套接字上有可读事件,你可以尝试读取数据。如果读取失败并且errno被设置为ECONNRESET或者recv()返回0,这表示连接已经断开。

3、如何建立可靠信令网络

3.1 客户端

客户端要健壮,则需要防止大量发送消息被丢失问题。客户端需要建立消息队列缓存消息,做防雪崩处理等。

3.2 服务端

通道建立了,但是服务端推送的事件想不丢失,则需要服务端有缓存队列,服务端发送失败则缓存下来,如果客户端收到并发送了ACK则从队列清除,如何清除还需要一定的机制配合。

相关推荐
小马爱打代码2 分钟前
TCP 详解
网络·网络协议·tcp/ip
聿琴惜荭顏丶43 分钟前
.NET MAUI进行UDP通信(二)
网络协议·udp·.net
hkNaruto2 小时前
【P2P】基于 Nebula 的 P2P 通信技术的虚拟局域网游戏设计方案
网络协议·游戏·p2p
Themberfue3 小时前
UDP/TCP ③-拥塞控制 || 滑动窗口 || 流量控制 || 快速重传
网络·网络协议·tcp/ip·计算机网络·udp
zhu09021501024 小时前
minio https配置
网络协议·http·https
鹅肝手握高V五色4 小时前
免费代理抓包工具SniffMaster(嗅探大师)抓取https
网络协议·http·https
Zfox_5 小时前
应用层协议 HTTP 讲解&实战:从0实现HTTP 服务器
linux·服务器·网络·c++·网络协议·http
前端没钱6 小时前
flutter入门系列教程<2>:Http请求库-dio的使用
网络协议·flutter·http
幽兰的天空15 小时前
介绍 HTTP 请求如何实现跨域
网络·网络协议·http
lisenustc15 小时前
HTTP post请求工具类
网络·网络协议·http