Socket编程

Socket编程

Socket预备知识

端口号

1. 端口号的作用
  • 标识应用程序:在一台设备上,可能有多个应用程序同时使用网络。端口号用于区分这些应用程序。
  • 多路复用:通过端口号,多个应用程序可以共享同一个IP地址。
  • 确保数据准确交付:端口号与IP地址结合,确保数据能够准确地传输到目标设备的特定应用程序。

2. 端口号与进程的关系
  • 进程是操作系统中正在运行的程序的实例。每个进程可以绑定一个或多个端口号,用于接收和发送数据。
  • 端口号是进程与网络通信的桥梁:
    • 服务器进程:通常绑定到一个固定的端口号,等待客户端连接。
    • 客户端进程:通常使用动态端口号,与服务器通信。
示例
  • 当你在浏览器中访问一个网站时:
    • 浏览器(客户端进程)会使用一个动态端口号(如50000)与服务器的80端口(HTTP服务)通信。
    • 服务器上的Web服务器进程(如Apache或Nginx)监听80端口,处理浏览器的请求。

3. 端口号的分类

端口号是一个16位的整数,范围是 0~65535。根据用途,端口号可以分为以下几类:

(1)知名端口(Well-Known Ports)
  • 范围:0~1023
  • 用途:预留给系统或知名服务使用。
  • 示例:
    • 21:FTP(文件传输协议)
    • 22:SSH(安全外壳协议)
    • 80:HTTP(超文本传输协议)
    • 443:HTTPS(安全的HTTP)
(2)注册端口(Registered Ports)
  • 范围:1024~49151
  • 用途:预留给用户或应用程序使用,但需要向IANA(互联网编号分配机构)注册。
  • 示例:
    • 3306:MySQL数据库
    • 8080:HTTP备用端口
    • 27017:MongoDB数据库
(3)动态端口(Dynamic Ports)
  • 范围:49152~65535
  • 用途:临时分配给客户端应用程序使用,通常用于短暂的通信。
  • 示例:
    • 客户端浏览器访问网站时,会使用动态端口与服务器的80端口通信。

Socket

  • 综上,IP地址用来标识互联网中唯一一台主机,port用来标识该主机上的唯一一个网络进程。
  • IP+Port 就能表示互联网中的唯一一个进程。
  • 所以,通信的时候,本质就是两个互联网进程代表人来通信,{srcIp,srcPort,dstIp,dstPort}这样的4元组就能标识互联网中唯二的两个进程。
  • 所以网络通信的本质就是进程间通信
  • 我们把Ip+Port叫做套接字Socket

TCP(传输控制协议)

  • 特点:面向连接、可靠、基于字节流。
  • 工作原理
    • 通信前需要建立连接(三次握手)。
    • 数据传输过程中保证数据不丢失、不重复、按顺序到达。
    • 通信结束后断开连接(四次挥手)。
  • 适用场景:需要高可靠性的应用,如网页浏览、文件传输、电子邮件等。

UDP(用户数据报协议)

  • 特点:无连接、不可靠、基于数据报。
  • 工作原理
    • 通信前不需要建立连接。
    • 数据传输速度快,但不保证可靠性(可能丢失、乱序或重复)。
    • 适合对实时性要求高的场景。
  • 适用场景:视频流、在线游戏、DNS查询等。

网络字节序

网络字节序(Network Byte Order)是网络通信中用于标准化数据传输的字节序(Byte Order),它规定了数据在网络中传输时的字节排列顺序。网络字节序采用大端序(Big-Endian),即高位字节存储在低地址,低位字节存储在高地址。


1. 字节序的概念

字节序是指多字节数据在内存中的存储顺序,主要分为两种:

  1. 大端序(Big-Endian)
    • 高位字节存储在低地址,低位字节存储在高地址。
    • 例如,整数0x12345678在内存中的存储顺序为:12 34 56 78
  2. 小端序(Little-Endian)
    • 低位字节存储在低地址,高位字节存储在高地址。
    • 例如,整数0x12345678在内存中的存储顺序为:78 56 34 12

2. 网络字节序的作用
  • 标准化数据传输:不同计算机可能使用不同的字节序(如x86架构使用小端序,而某些嵌入式系统使用大端序)。为了确保数据在不同设备之间正确传输,网络协议规定使用大端序作为网络字节序。
  • 避免兼容性问题:通过统一使用网络字节序,可以避免因字节序不同而导致的数据解析错误。

3. 相关函数

在网络编程中,操作系统提供了一组函数用于主机字节序和网络字节序之间的转换:

(1)主机字节序到网络字节序
  • htons():将16位整数(如端口号)从主机字节序转换为网络字节序。
  • htonl():将32位整数(如IP地址)从主机字节序转换为网络字节序。
(2)网络字节序到主机字节序
  • ntohs():将16位整数从网络字节序转换为主机字节序。
  • ntohl():将32位整数从网络字节序转换为主机字节序。
uint16_t htons(uint16_t hostshort);
uint32_t htonl(uint32_t hostlong);
uint16_t ntohs(uint16_t netshort);
uint32_t ntohl(uint32_t netlong);

4. 示例

以下是一个简单的示例,展示如何使用这些函数:

#include <stdio.h>
#include <arpa/inet.h>

int main() {
    uint16_t host_port = 8080;
    uint32_t host_ip = 0x12345678;

    // 主机字节序到网络字节序
    uint16_t net_port = htons(host_port);
    uint32_t net_ip = htonl(host_ip);

    printf("Host port: %x -> Network port: %x\n", host_port, net_port);
    printf("Host IP: %x -> Network IP: %x\n", host_ip, net_ip);

    // 网络字节序到主机字节序
    uint16_t back_port = ntohs(net_port);
    uint32_t back_ip = ntohl(net_ip);

    printf("Network port: %x -> Host port: %x\n", net_port, back_port);
    printf("Network IP: %x -> Host IP: %x\n", net_ip, back_ip);

    return 0;
}

Socket编程接口

以下是Socket编程中五个常见API的接口用法说明:


Socket编程常见API
1. socket()
  • 功能:创建一个Socket。
  • 接口
    int socket(int domain, int type, int protocol);
    
  • 参数
    • domain:协议族,如AF_INET(IPv4)或AF_INET6(IPv6)。
    • type:Socket类型,如SOCK_STREAM(TCP)或SOCK_DGRAM(UDP)。
    • protocol:协议,通常为0(自动选择)。
  • 返回值:成功返回Socket文件描述符,失败返回-1

2. bind()
  • 功能:将Socket绑定到特定的IP地址和端口号。

  • 接口

    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    
  • 参数

    • sockfd:Socket文件描述符。
    • addr:指向sockaddr结构的指针,包含IP地址和端口号。
    • addrlenaddr结构的大小。
  • 返回值:成功返回0,失败返回-1


3. listen()
  • 功能:将Socket设置为监听模式,等待客户端连接。
  • 接口
    int listen(int sockfd, int backlog);
    
  • 参数
    • sockfd:Socket文件描述符。
    • backlog:等待连接队列的最大长度。
  • 返回值:成功返回0,失败返回-1

4. accept()
  • 功能:接受客户端的连接请求。
  • 接口
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    
  • 参数
    • sockfd:监听Socket的文件描述符。
    • addr:指向sockaddr结构的指针,用于存储客户端地址信息。
    • addrlen:指向addr结构大小的指针。
  • 返回值:成功返回新的Socket文件描述符,失败返回-1

5. connect()
  • 功能:客户端连接到服务器。

  • 接口

    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    
  • 参数

    • sockfd:Socket文件描述符。
    • addr:指向sockaddr结构的指针,包含服务器的IP地址和端口号。
    • addrlenaddr结构的大小。
  • 返回值:成功返回0,失败返回-1


以下是按照你提供的格式对 inet_ntopinet_addrsendtorecvfrom 的详细介绍:


6. inet_ntop()
  • 功能:将二进制格式的 IP 地址(如 struct in_addrstruct in6_addr)转换为可读的字符串格式(如 "192.168.1.1""2001:db8::1")。

  • 接口

    #include <arpa/inet.h>
    
    const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
    
  • 参数

    • af:地址族,AF_INET(IPv4)或 AF_INET6(IPv6)。
    • src:指向二进制格式的 IP 地址(如 struct in_addr)。
    • dst:指向存储结果的字符串缓冲区。
    • size:缓冲区的大小。
  • 返回值:成功返回指向 dst 的指针,失败返回 NULL

  • 示例

    struct in_addr addr;
    addr.s_addr = inet_addr("192.168.1.1");
    char ip_str[INET_ADDRSTRLEN];
    const char *result = inet_ntop(AF_INET, &addr, ip_str, INET_ADDRSTRLEN);
    if (result) {
        printf("IP address: %s\n", ip_str);
    }
    

7. inet_addr()
  • 功能:将点分十进制格式的 IPv4 地址字符串(如 "192.168.1.1")转换为 32 位的二进制格式(in_addr_t),结果为网络字节序。

  • 接口

    #include <arpa/inet.h>
    
    in_addr_t inet_addr(const char *cp);
    
  • 参数

    • cp:点分十进制格式的 IPv4 地址字符串。
  • 返回值:成功返回 32 位的二进制 IP 地址(网络字节序),失败返回 INADDR_NONE(通常是 -1)。

  • 示例

    in_addr_t addr = inet_addr("192.168.1.1");
    if (addr != INADDR_NONE) {
        printf("Binary IP: %u\n", addr);
    }
    

8. sendto()
  • 功能:向指定的目标地址发送数据,通常用于无连接套接字(如 UDP)。

  • 接口

    #include <sys/types.h>
    #include <sys/socket.h>
    
    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                   const struct sockaddr *dest_addr, socklen_t addrlen);
    
  • 参数

    • sockfd:套接字文件描述符。
    • buf:指向要发送的数据缓冲区。
    • len:要发送的数据长度。
    • flags:控制发送行为的标志位,通常设置为 0
    • dest_addr:指向目标地址的 sockaddr 结构体。
    • addrlen:目标地址结构体的大小。
  • 返回值:成功返回实际发送的字节数,失败返回 -1

  • 示例

    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    server_addr.sin_addr.s_addr = inet_addr("192.168.1.1");
    
    const char *message = "Hello, Server!";
    ssize_t sent = sendto(sockfd, message, strlen(message), 0,
                          (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (sent < 0) {
        perror("sendto failed");
    }
    

9. recvfrom()
  • 功能:从套接字接收数据,并获取发送方的地址信息,通常用于无连接套接字(如 UDP)。

  • 接口

    #include <sys/types.h>
    #include <sys/socket.h>
    
    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                     struct sockaddr *src_addr, socklen_t *addrlen);
    
  • 参数

    • sockfd:套接字文件描述符。
    • buf:指向接收数据的缓冲区。
    • len:缓冲区的最大长度。
    • flags:控制接收行为的标志位,通常设置为 0
    • src_addr:指向存储发送方地址的 sockaddr 结构体。
    • addrlen:指向 socklen_t 类型的变量,表示 src_addr 的大小。
  • 返回值:成功返回实际接收的字节数,失败返回 -1

  • 示例

    struct sockaddr_in client_addr;
    socklen_t addrlen = sizeof(client_addr);
    char buffer[1024];
    
    ssize_t received = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
                                (struct sockaddr *)&client_addr, &addrlen);
    if (received > 0) {
        buffer[received] = '\0';
        printf("Received: %s\n", buffer);
    }
    

总结
  • socket():创建Socket。
  • bind():绑定IP和端口。
  • listen():监听连接。
  • accept():接受连接。
  • connect():发起连接。
sockaddr结构

Socket边包含三种编程

  • 网络Socket (本地+网络)
  • 本地Socket (unix域间socket,类似管道)
  • 原始Socket (设计网络工具,我们这里不考虑)

网络常见命令


1. ping 命令

作用:测试与目标主机的网络连通性。
原理:发送 ICMP 回显请求包,等待目标主机返回 ICMP 回显应答包。
常用选项

  • -c <次数>:指定发送的 ICMP 包数量。
    ping -c 4 google.com
    
  • -i <间隔>:设置发送 ICMP 包的间隔时间(秒)。
    ping -i 2 google.com
    
  • -s <大小>:设置 ICMP 包的大小(字节)。
    ping -s 100 google.com
    

示例

ping google.com

2. netstat 命令

作用:显示网络连接、路由表、接口统计信息等。
常用选项

  • -t:显示 TCP 连接。
  • -u:显示 UDP 连接。
  • -n:以数字形式显示地址和端口。
  • -l:显示监听中的套接字。
  • -p:显示进程 ID 和程序名称。
  • -a:显示所有连接和监听端口。

联合watch命令可以实现对网络的实时监控

watch -n 1 netstat -nuap

示例

  • 查看所有 TCP 连接:
    netstat -t
    
  • 查看所有监听端口:
    netstat -l
    
  • 查看所有连接(包括进程信息):
    netstat -tunap
    

3. pidof 命令

作用:根据进程名称获取进程 ID(PID)。
常用选项

  • -s:仅返回一个 PID。
  • -x:匹配脚本名称。

示例

  • 获取 nginx 进程的 PID:
    pidof nginx
    
  • 获取 bash 进程的 PID:
    pidof bash
    

UDP Socket编程

UDP编程流程

服务端

  1. 创建套接字 socket()
  2. 填充套接字结构体sockaddr_in信息,并绑定 bind()
  3. 启动后在while(true)内部 recvfrom接收信息,sendto发送信息。

客户端

  1. 创捷套接字 socket()
  2. 不需要显示绑定,在第一次调用sendto的时候os会自动绑定
  3. sendto发送消息 recvfrom接收信息

1.基于 UDP 的简单客户端-服务器通信实现

使用 C++ 实现一个基于 UDP 的简单客户端-服务器通信系统。


项目结构

项目主要由以下几个文件组成:

  1. UdpClientMain.cc:客户端主程序。
  2. UdpServer.hpp:服务器类的定义。
  3. UdpServerMain.cc:服务器主程序。
  4. InetAddr.hpp:网络地址封装类。
  5. Comman.hpp:通用常量和工具函数。

核心类分析
1. InetAddr

InetAddr 类封装了网络地址(IP 和端口号)的转换和管理,主要功能包括:

  • sockaddr_in 结构体转换为人类可读的 IP 和端口号。
  • 将本地字节序的端口号转换为网络字节序。
  • 提供 Addr()Len() 方法,方便在套接字编程中使用。

关键代码:

class InetAddr {
public:
    InetAddr(const struct sockaddr_in &addr) : _net_addr(addr) {
        PortNet2Host();
        IpNet2Host();
    }

    InetAddr(const uint16_t port) : _port(port), _ip("") {
        memset(&_net_addr, 0, sizeof(_net_addr));
        _net_addr.sin_family = AF_INET;
        _net_addr.sin_port = htons(_port);
        _net_addr.sin_addr.s_addr = INADDR_ANY;
    }

    struct sockaddr *Addr() { return CONV(&_net_addr); }
    socklen_t Len() { return sizeof(_net_addr); }
    std::string Ip() { return _ip; }
    uint16_t Port() { return _port; }

private:
    struct sockaddr_in _net_addr;
    std::string _ip;
    uint16_t _port;
};

2. UdpServer

UdpServer 类实现了 UDP 服务器的核心功能,包括:

  • 创建和绑定套接字。
  • 接收客户端消息并回显。

关键代码:

class UdpServer {
public:
    UdpServer(int port) : _addr(port), _isrunning(false), _sockfd(gsockfd) {}

    void InitServer() {
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0) {
            LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
            DIE(SOCKET_ERR);
        }

        int n = bind(_sockfd, CONV(_addr.Addr()), _addr.Len());
        if (n < 0) {
            LOG(LogLevel::FATAL) << "bind: " << strerror(errno);
            DIE(BIND_ERR);
        }
    }

    void Start() {
        _isrunning = true;
        while (true) {
            char inbuffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);

            int n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, CONV(&peer), &len);
            if (n > 0) {
                InetAddr cli(peer);
                inbuffer[n] = 0;
                std::string clinetInfo = cli.Ip() + ":" + std::to_string(cli.Port()) + "#" + inbuffer;
                LOG(LogLevel::INFO) << clinetInfo;

                std::string echoInfo = "echo# " + clinetInfo;
                ::sendto(_sockfd, echoInfo.c_str(), echoInfo.size(), 0, CONV(&peer), len);
            }
        }
    }

    ~UdpServer() {
        if (_sockfd > gsockfd)
            ::close(_sockfd);
    }

private:
    int _sockfd;
    bool _isrunning;
    InetAddr _addr;
};

3. UdpClientMain 主程序

客户端主程序负责与服务器通信,主要功能包括:

  • 创建套接字。
  • 发送用户输入的数据到服务器。
  • 接收服务器的回显数据并打印。

关键代码:

int main(int argc, char *argv[]) {
    if (argc != 3) {
        std::cerr << "Usage: " << argv[0] << " serverip serverport" << std::endl;
        DIE(USAGE_ERR);
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        std::cerr << "socket error" << std::endl;
        DIE(SOCKET_ERR);
    }

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    server.sin_port = htons(serverport);

    while (true) {
        std::cout << "Please Enter# ";
        std::string message;
        getline(std::cin, message);

        int n = sendto(sockfd, message.c_str(), message.size(), 0, CONV(&server), sizeof(server));

        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buffer[1024];
        n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&temp), &len);
        if (n > 0) {
            buffer[n] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return 0;
}

运行流程
  1. 启动服务器

    • 运行 UdpServerMain,绑定到指定端口。
    • 等待客户端消息。
  2. 启动客户端

    • 运行 UdpClientMain,指定服务器 IP 和端口。
    • 输入消息并发送到服务器。
  3. 通信过程

    • 客户端发送消息到服务器。
    • 服务器接收消息并回显。
    • 客户端接收回显消息并打印。

示例运行
  1. 启动服务器:

    ./UdpServerMain 8080
    
  2. 启动客户端:

    ./UdpClientMain 127.0.0.1 8080
    
  3. 在客户端输入消息:

    Please Enter# Hello, Server!
    
  4. 客户端显示回显消息:

    echo# 127.0.0.1:12345#Hello, Server!
    

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

示例总结

​ 该小项目简单实现了服务端 - 客户端的通信,下面来梳理一下整体的运行逻辑和一些工具类的实现思路。

​ 首先服务端,由于服务端的特性,我们需要服务端的端口号是稳定不变的,所以我们创建完套接字后,需要手动bind端口号,如果端口号绑定失败就需要程序直接退出,并返回错误码。在bind之前,我们需要先填充一下我们的sockaddr_in信息,由于服务端的特殊性我们的端口号是稳定不变的所以就可以直接把本地指定的port号用htons转成网络字节序,此外,一台服务器上可能有多个网卡IP地址可能不一样,但只要是发到我们指定端口号上的信息我们都要提取,所以ip号可以设置为0.0.0.0在代码中是宏INADDR_ANY。填充完后就可以进行bind,传参时需要进行强转,由于sockaddr_in强转到sockaddr频率很高,所以单独封成一个函数到comman.hpp当中。这是服务端的初始化,服务端初始化后就是启动逻辑。服务端与客户端通信的过程中,肯定是客户端先发送信息,所以服务端先用recvfrom等待服务端发送的信息,并且可以通过创建struct sockaddr对象peer传入函数来拿到客户端的套接字信息,收到信息后客户端再做一定的处理动作,再用sendto向客户端发送信息,并再次传入peer套接字,来告诉os发送数据的目标套接字。

​ 然后是客户端,客户端运行一定会要求客户端知道服务端的套接字信息也就是IP地址和Port,客户端要率先向服务端发送信息,sendto函数需要套接字地址sockaddr,所以客户端先填充服务端套接字数据,然后再进行sendto传输信息,客户端不需要显示的bind,在第一次调用sendto的时候客户端os会自动分配一个没有被占用的port进行绑定,发送数据后客户端等待服务端响应数据,在recvfrom的时候也要创建一个sockaddr_in对象来接收源套接字数据。

2.基于UDP的群聊系统设计与实现

一、观察者模式的应用

在这个群聊项目中,观察者模式的实现非常巧妙:

  1. 观察者(Observer)- User类:
class User : public UserInterface {
public:
    void Sendto(int sockfd, const std::string &message) override {
        // 接收并处理来自UserManager的消息
        sendto(sockfd, message.c_str(), message.size(), 0, _id.NetAddr(), _id.Len());
    }
};
  1. 被观察者(Subject)- UserManager类:
class UserManager {
private:
    std::list<std::shared_ptr<UserInterface>> _online_user;  // 观察者列表
public:
    void AddUser(InetAddr &id) {  // 添加观察者
        _online_user.push_back(std::make_shared<User>(id));
    }
    
    void Router(int sockfd, const std::string &message) {  // 通知所有观察者
        for (auto &user : _online_user) {
            user->Sendto(sockfd, message);
        }
    }
};

观察者模式的体现:

  • UserManager维护了一个观察者(User)列表
  • 当有新消息时,UserManager通过Router方法通知所有观察者
  • 每个User都实现了接收消息的接口(Sendto)
  • 实现了消息的一对多分发
二、项目实现逻辑
1. 整体架构
UdpServer(服务器核心)
    ↓
UserManager(用户管理)
    ↓
User(单个用户)
2. 核心功能流程
  1. 服务器初始化:
void InitServer() {
    _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    bind(_sockfd, CONV(_addr.NetAddr()), _addr.Len());
}
  1. 消息接收与处理:
void Start() {
    while (true) {
        recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0, CONV(&peer), &len);
        _adduser(cli);  // 添加新用户
        // 使用线程池处理消息转发
        ThreadPool<task_t>::getInstance()->Equeue([this,message](){
            _route(_sockfd,message);
        });
    }
}
三、回调函数设计

项目中使用了多个回调函数来解耦各个模块:

  1. 用户添加回调:
using adduser_t = std::function<void(InetAddr &id)>;
  1. 消息路由回调:
using route_t = std::function<void(int sockfd, const std::string &message)>;

回调函数的优势:

  • 实现了模块间的松耦合
  • 提高了代码的可维护性和可扩展性
  • 使功能更加灵活可配置
四、UDP套接字特性
  1. UDP特点:
  • 无连接
  • 不可靠传输
  • 高效快速
  • 适合广播/多播
  1. 在项目中的应用:
// 发送消息
sendto(sockfd, message.c_str(), message.size(), 0, addr, len);

// 接收消息
recvfrom(sockfd, buffer, size, 0, addr, &len);
  1. UDP的优势:
  • 适合群聊场景的消息广播
  • 无需维护连接状态
  • 实现简单,开销小
总结

该项目是使用UDP套接字来粗略实现了一个建议的群聊通信功能,由于是粗略实现所以我们直接用IP地址来定义一个用户,我们使用观察者模式来实现,该UDP群聊系统的执行流程是:当服务器启动时,首先创建UserManager(被观察者)和UdpServer实例,并通过回调函数将两者关联起来;当客户端发送消息时,UdpServer通过recvfrom接收消息,然后通过adduser回调函数将新用户添加到UserManager的观察者列表中(此时User作为观察者被创建并加入列表),接着消息会被提交到线程池中,通过route回调函数触发UserManager的Router方法,Router方法会遍历所有在线用户(观察者列表),调用每个User对象的Sendto方法(观察者的更新方法)将消息转发给所有客户端。在这个过程中,观察者模式体现在:UserManager作为被观察者维护了一个User观察者列表,当有新消息时,通过Router方法通知所有观察者,而每个User观察者通过实现UserInterface接口中的Sendto方法来处理接收到的通知(消息),实现了消息的一对多分发,同时通过回调函数机制实现了UdpServer和UserManager之间的解耦,使得系统具有良好的可扩展性和可维护性。

并且route转发方法使用线程池来实现,在通信的过程当中,如果只有主线程来进行消息的转发那就必须等到消息转发完之后才能继续收信息,但用线程池的话就可以一直收信息,然后把任务push到任务队列当中,让其他线程来进行处理。

TCP Socket编程

TCP编程流程

服务端

  1. 创建套接字 socket()
  2. 填充套接字结构体sockaddr_in信息,并绑定 bind()
  3. 设置为监听状态 listen()
  4. write read 或者 send recv 读取发送信息

客户端

  1. 创建套接字 socket()
  2. 填充服务端sockaddr信息并建立连接 connect()

该项目是使用UDP套接字来粗略实现了一个建议的群聊通信功能,由于是粗略实现所以我们直接用IP地址来定义一个用户,我们使用观察者模式来实现,该UDP群聊系统的执行流程是:当服务器启动时,首先创建UserManager(被观察者)和UdpServer实例,并通过回调函数将两者关联起来;当客户端发送消息时,UdpServer通过recvfrom接收消息,然后通过adduser回调函数将新用户添加到UserManager的观察者列表中(此时User作为观察者被创建并加入列表),接着消息会被提交到线程池中,通过route回调函数触发UserManager的Router方法,Router方法会遍历所有在线用户(观察者列表),调用每个User对象的Sendto方法(观察者的更新方法)将消息转发给所有客户端。在这个过程中,观察者模式体现在:UserManager作为被观察者维护了一个User观察者列表,当有新消息时,通过Router方法通知所有观察者,而每个User观察者通过实现UserInterface接口中的Sendto方法来处理接收到的通知(消息),实现了消息的一对多分发,同时通过回调函数机制实现了UdpServer和UserManager之间的解耦,使得系统具有良好的可扩展性和可维护性。

并且route转发方法使用线程池来实现,在通信的过程当中,如果只有主线程来进行消息的转发那就必须等到消息转发完之后才能继续收信息,但用线程池的话就可以一直收信息,然后把任务push到任务队列当中,让其他线程来进行处理。

TCP Socket编程

TCP编程流程

服务端

  1. 创建套接字 socket()
  2. 填充套接字结构体sockaddr_in信息,并绑定 bind()
  3. 设置为监听状态 listen()
  4. write read 或者 send recv 读取发送信息

客户端

  1. 创建套接字 socket()
  2. 填充服务端sockaddr信息并建立连接 connect()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值