<11>_Linux网络编程套接字

目录

一、网络编程相关概念

1,理解源IP地址和目的IP地址

2,认识端口号

3,理解端口号和进程ID

4,理解源端口号和目的端口号

5,认识TCP协议

6,认识UDP协议

7,理解网络字节

二、socket编程接口

1,socket 常见API

2,sockaddr结构

三、简单的UDP网络程序

1,返回消息的echo服务器

2,远程接受指令的服务器

3,转发客户端消息的服务器

四、简单的TCP网络程序

1,单进程版的服务器

2,多进程版的服务器

3,多线程版的服务器

4,线程池版的服务器

五、TCP协议通讯流程

1,服务器初始化:

2,建立连接的过程:

3,数据传输的过程:

4,断开连接的过程:

5,应用程序和TCP协议层的交互过程

6,TCP 和 UDP 对比


一、网络编程相关概念

1,理解源IP地址和目的IP地址

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址.

思考: 我们光有IP地址就可以完成通信了嘛?

想象一下发qq消息的例子, 有了IP地址能够把消息发送到对方的机器上,

但是还需要有一个其他的标识来区分出, 这个数据要给哪个程序进行解析.

2,认识端口号

端口号(port)是传输层协议的内容.

端口号是一个2字节16位的整数;

端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;

IP地址+ 端口号能够标识网络上的某一台主机的某一个进程;

一个端口号只能被一个进程占用.

3,理解端口号和进程ID

一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定;

此处我们的端口号也是唯一表示一个进程. 那么这 两者之间是怎样的关系?

10086的例子:

我们打电话给电信充值,拨10086,客服23号接通电话。这个10086就是进程ID,客服编号23就是端口号。

一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定;

4,理解源端口号和目的端口号

传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号.

就是在描述"数据是谁发的, 要发给谁";

发送者是源端口号,接收者是目的端口号。

5,认识TCP协议

此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题.

传输层协议

有连接

可靠传输

面向字节流

6,认识UDP协议

此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后面再详细讨论.

传输层协议

无连接

不可靠传输

面向数据报

7,理解网络字节

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;

接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;

因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.

TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.

不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;

如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数,做网络字节序和主机字节序的转换。

字节序转化函数:

些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。

例如htonl表示将32位的长整数从主机字节序转换为网络字节序,

例如将IP地址转换后准备发送。

如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;

如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

二、socket编程接口

1,socket 常见API

// 创建  socket 文件描述符  (TCP/UDP, 客户端 + 服务器) 
int socket(int domain, int type, int protocol);

// 绑定端口号  (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);

// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);

// 接收请求  (TCP, 服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);

// 建立连接  (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

2,sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同.

socket接口的三种结构:

IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16 位端口号和32位IP地址.

IPv4、 IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址, 不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.

socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;

sockaddr 结构

/* Structure describing a generic socket address.  */
struct sockaddr
  {
    __SOCKADDR_COMMON (sa_);	/* Common data: address family and length.  */
    char sa_data[14];		/* Address data.  */
  };

sockaddr_in 结构

/* Structure describing an Internet socket address.  */
struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;			/* Port number.  */
    struct in_addr sin_addr;		/* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
			   __SOCKADDR_COMMON_SIZE -
			   sizeof (in_port_t) -
			   sizeof (struct in_addr)];
  };

in_addr结构

/* Internet address.  */
typedef uint32_t in_addr_t;
struct in_addr
  {
    in_addr_t s_addr;
  };

三、简单的UDP网络程序

1,返回消息的echo服务器

// 服务端
[user@iZwz9eoohx59fs5a6ampomZ udp]$ cat udp_server.hpp 
#ifndef _UDP_SAERVAER_HPP
#define _UDP_SAERVAER_HPP

#include "log.hpp"
#include <iostream>
#include <cstdio>
#include <string>
#include <cerrno>
#include <cstring>
#include <stdlib.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define SIZE 1024

class UdpServer
{
public:
    // UdpServer(uint16_t port, std::string ip="0.0.0.0")
    UdpServer(uint16_t port, std::string ip= "")
    : _port(port),
      _ip(ip),
      _sock(-1)
    {}
    ~UdpServer()
    {
        if(_sock >= 0) close(_sock);
    }
    bool initServer()
    {
        // 从这里开始,就是新的系统调用,来完成网络功能啦
        // 1.创建套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0); //AF_INET和PF_INET是一样的
        if(_sock < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        // 2.设置好缓冲区,把ip和port从主机序列转换到网络序列
        // "192.168.1.3" -> 点分十进制字符串IP地址
        // 每一个区域的取值范围[0-255]: 1个字节 -> 4个区域
        // 理论上,标识一个IP地址,其实4个字节就够了
        // 点分十进制字符串风格的IP地址 -> 4个字节
        struct sockaddr_in local;
        bzero(&local, sizeof(local));//结构体清零
        local.sin_family = AF_INET;  //服务器的IP和端口未来也是要发送给对方主机的 -> 先要将数据发送到网络
        local.sin_port = htons(_port);//主机序列转成网络序列,规定大小端
        // 同上,先要将点分十进制字符串风格的IP地址 转成-> 4字节
        // 然后再把,4字节主机序列 转成-> 网络序列
        // inet_addr接口可以把 十进制字符串 转成-> 网络序列
        // 还能让服务器在工作的过程在,可以从任意IP中获取数据
        // INADDR_ANY宏就是IP 0.0.0.0
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY :  inet_addr(_ip.c_str());
        
        // 3.bind:将用户设置的ip和port在内核中,要和我们当前的进程强关联
        if(bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        // 4.初始化成功,提示信息
        logMessage(NORMAL, "init udp serverdone ... %s", strerror(errno));
        return true;
    }
    void Start()
    {
        // 作为一款网络服务器,是永远不退出的
        // 服务器启动 -> 常驻进程 -> 永远在内存中存在,除非挂了
        // echo服务器,客户端给服务端发消息,原封不动的返回消息即可
        // 1.设置缓冲区
        char buffer[SIZE];
        for(;;)
        {
            //peer为纯输出型参数
            struct sockaddr_in peer;
            bzero(&peer,sizeof(peer));
            //len = sizeof(peer)固定写法
            //len是输入输出型参数
            //输入是:peer的缓冲区大小
            //输出是:实际读到的peer
            socklen_t len = sizeof(peer);
            
            //2.读取数据
            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
            if(s > 0)
            {
                buffer[s] = 0; // 我们目前的数据当做字符串
                uint16_t cli_port = ntohs(peer.sin_port); //从网络中来的,要把网络序列转成主机序列
                std::string cli_ip = inet_ntoa(peer.sin_addr);//4字节网络序列IP转成本主机字符串风格的IP
                printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer); //显示传输来的信息
            }
            //3.处理数据

            //4.发送数据
            sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
        }
    }
private:
    // 一个服务器,一般必须需要ip地址和port端口号
    std::string _ip; // ip地址
    uint16_t _port;  // port端口号
    int _sock;       // 通信套接字
};

#endif
[user@iZwz9eoohx59fs5a6ampomZ udp]$ cat udp_server.cc
#include "udp_server.hpp"
#include <memory>
#include <cstdlib>

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " Set_Ip Set_Port\n" << std::endl;
}

int main(int argc, char* argv[])
{
    // if(argc != 3)
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    
    // std::string ip = argv[1];
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port));

    svr->initServer();
    svr->Start();

    return 0;
}
[user@iZwz9eoohx59fs5a6ampomZ udp]$ cat udp_client.cc
#include "udp_server.hpp"
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " Server_Ip Server_Port\n" << std::endl;
}

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }
    
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if(sock < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    std::string message;
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);

    char buffer[1024];
    while(true)
    {
        std::cout << "请输入你的信息# ";
        std::getline(std::cin, message);
        if(message == "quit") break;
        //当client首次发送消息给服务器,OS会自动给Client bind它的IP和PORT
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));

        struct sockaddr_in temp; 
        socklen_t len = sizeof(temp);        
        ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr*)&temp, &len);
        if(s > 0)
        {
            buffer[s] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }
    return 0;
}
[user@iZwz9eoohx59fs5a6ampomZ udp]$ make clean;make
rm -f udp_client udp_server
g++ -o udp_client udp_client.cc -std=c++11
g++ -o udp_server udp_server.cc -std=c++11
[user@iZwz9eoohx59fs5a6ampomZ udp]$ ./udp_server 8080
[NORMAL] [1759499806] init udp serverdone ... Success
[127.0.0.1:39305]# 、hao
[127.0.0.1:39305]# 你好
[127.0.0.1:39305]# 在吗
[127.0.0.1:39305]# 吃了吗
[127.0.0.1:39305]# 拜拜
^C

//客户端
user@iZwz9eoohx59fs5a6ampomZ udp]$ ./udp_client 127.0.0.1 8080
请输入你的信息# nihao^H^H^H^H^H、
server echo# 、hao
请输入你的信息# 你好
server echo# 你好
请输入你的信息# 在吗
server echo# 在吗
请输入你的信息# 吃了吗
server echo# 吃了吗
请输入你的信息# 拜拜
server echo# 拜拜
请输入你的信息# quit
[user@iZwz9eoohx59fs5a6ampomZ udp]$

2,远程接受指令的服务器

[user@iZwz9eoohx59fs5a6ampomZ udp]$ cat udp_server.hpp 
#ifndef _UDP_SAERVAER_HPP
#define _UDP_SAERVAER_HPP

#include "log.hpp"
#include <iostream>
#include <cstdio>
#include <string>
#include <cerrno>
#include <cstring>
#include <stdlib.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define SIZE 1024

class UdpServer
{
public:
    // UdpServer(uint16_t port, std::string ip="0.0.0.0")
    UdpServer(uint16_t port, std::string ip= "")
    : _port(port),
      _ip(ip),
      _sock(-1)
    {}
    ~UdpServer()
    {
        if(_sock >= 0) close(_sock);
    }
    bool initServer()
    {
        // 从这里开始,就是新的系统调用,来完成网络功能啦
        // 1.创建套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0); //AF_INET和PF_INET是一样的
        if(_sock < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        // 2.设置好缓冲区,把ip和port从主机序列转换到网络序列
        // "192.168.1.3" -> 点分十进制字符串IP地址
        // 每一个区域的取值范围[0-255]: 1个字节 -> 4个区域
        // 理论上,标识一个IP地址,其实4个字节就够了
        // 点分十进制字符串风格的IP地址 -> 4个字节
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); //结构体清零
        local.sin_family = AF_INET;   //服务器的IP和端口未来也是要发送给对方主机的 -> 先要将数据发送到网络
        local.sin_port = htons(_port);//主机序列转成网络序列,规定大小端
        // 同上,先要将点分十进制字符串风格的IP地址 转成-> 4字节
        // 然后再把,4字节主机序列 转成-> 网络序列
        // inet_addr接口可以把 十进制字符串 转成-> 网络序列
        // 还能让服务器在工作的过程在,可以从任意IP中获取数据
        // INADDR_ANY宏就是IP 0.0.0.0
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        
        // 3.bind:将用户设置的ip和port在内核中,要和我们当前的进程强关联
        if(bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        // 4.初始化成功,提示信息
        logMessage(NORMAL, "init udp serverdone ... %s", strerror(errno));
        return true;
    }
    void Start()
    {
        // 作为一款网络服务器,是永远不退出的
        // 服务器启动 -> 常驻进程 -> 永远在内存中存在,除非挂了
        // echo服务器,客户端给服务端发消息,原封不动的返回消息即可
        // 1.设置缓冲区
        char buffer[SIZE];
        for(;;)
        {
            //peer为纯输出型参数
            struct sockaddr_in peer;
            bzero(&peer,sizeof(peer));
            //len = sizeof(peer)固定写法
            //len是输入输出型参数
            //输入是:peer的缓冲区大小
            //输出是:实际读到的peer
            socklen_t len = sizeof(peer);

            //2.读取数据
            char result[256];
            std::string cmd_echo;
            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
            if(s > 0)//读取成功
            {
                buffer[s] = 0; // 我们目前的数据当做字符串

                //发过来的消息是 ls -a -l
                if(strcasestr(buffer, "rm") != nullptr || strcasestr(buffer, "rm") != nullptr)
                {
                    std::string err_message = "非法信息....: ";
                    std::cout << err_message << buffer << std::endl;
                    sendto(_sock, err_message.c_str(), err_message .size(), 0, (struct sockaddr*)&peer, len);
                    continue;
                }

                FILE* fp = popen(buffer, "r");
                if(nullptr == fp)
                {
                    logMessage(ERROR, "popen:%d:%s", errno, strerror(errno));
                    continue;
                }

                while(fgets(result, sizeof(result), fp) != nullptr)
                {
                    cmd_echo += result;
                }
                fclose(fp);

                // uint16_t cli_port = ntohs(peer.sin_port); //从网络中来的,要把网络序列转成主机序列
                // std::string cli_ip = inet_ntoa(peer.sin_addr);//4字节网络序列IP转成本主机字符串风格的IP
                // printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer); //显示传输来的信息
            }
            //3.处理数据

            //4.发送数据
            // sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
            sendto(_sock, cmd_echo.c_str(), cmd_echo.size(), 0, (struct sockaddr*)&peer, len);
        }
    }
private:
    // 一个服务器,一般必须需要ip地址和port端口号
    std::string _ip; // ip地址
    uint16_t _port;  // port端口号
    int _sock;       // 通信套接字
};

#endif
[user@iZwz9eoohx59fs5a6ampomZ udp]$ cat udp_server.cc 
#include "udp_server.hpp"
#include <memory>
#include <cstdlib>

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " Set_Ip Set_Port\n" << std::endl;
}

int main(int argc, char* argv[])
{
    if(argc != 3)
    // if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    
    std::string ip = argv[1];
    uint16_t port = atoi(argv[2]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port, ip));

    svr->initServer();
    svr->Start();

    return 0;
}
[user@iZwz9eoohx59fs5a6ampomZ udp]$ cat udp_client.cc
#include "udp_server.hpp"
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " Server_Ip Server_Port\n" << std::endl;
}

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }

    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if(sock < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    std::string message;
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);

    char buffer[1024];
    while(true)
    {
        std::cout << "请输入你的信息# ";
        std::getline(std::cin, message);
        if(message == "quit") break;
        //当client首次发送消息给服务器,OS会自动给Client bind它的IP和PORT
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));

        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr*)&temp, &len);
        if(s > 0)
        {
            buffer[s] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }
    return 0;
}
[user@iZwz9eoohx59fs5a6ampomZ udp]$ make clean;make
rm -f udp_client udp_server
g++ -o udp_client udp_client.cc -std=c++11
g++ -o udp_server udp_server.cc -std=c++11
[user@iZwz9eoohx59fs5a6ampomZ udp]$ ./udp_server 127.0.0.1 8080
[NORMAL] [1759652247] init udp serverdone ... Success
sh: nihao: command not found
sh: hahahahahah: command not found
sh: $'66666666\b\b\b': command not found
sh: $'\E[A': command not found
非法信息....: rmdir abc
非法信息....: rm test.txt

//客户端
[user@iZwz9eoohx59fs5a6ampomZ udp]$ ./udp_client 127.0.0.1 8080
请输入你的信息# nihao
请输入你的信息# hahahahahah
请输入你的信息# 66666666^H^H^H
请输入你的信息# ls
server echo# log.hpp
Makefile
udp_client
udp_client.cc
udp_server
udp_server.cc
udp_server.hpp

请输入你的信息# ls -ai
server echo# 1704724 .
1704491 ..
1704726 log.hpp
1704723 Makefile
1704521 udp_client
1704706 udp_client.cc
1704545 udp_server
1704719 udp_server.cc
1704720 udp_server.hpp

请输入你的信息# touch test.txt
请输入你的信息# ls -ai
server echo# 1704724 .
1704491 ..
1704726 log.hpp
1704723 Makefile
1704552 test.txt
1704521 udp_client
1704706 udp_client.cc
1704545 udp_server
1704719 udp_server.cc
1704720 udp_server.hpp

请输入你的信息# mkdir abc
请输入你的信息# ls -ai
server echo# 1704724 .
1704491 ..
1704553 abc
1704726 log.hpp
1704723 Makefile
1704552 test.txt
1704521 udp_client
1704706 udp_client.cc
1704545 udp_server
1704719 udp_server.cc
1704720 udp_server.hpp

请输入你的信息# rmdir abc
server echo# 非法信息....: 
请输入你的信息# rm test.txt
server echo# 非法信息....: 
请输入你的信息# ls -ai
server echo# 1704724 .
1704491 ..
1704553 abc
1704726 log.hpp
1704723 Makefile
1704552 test.txt
1704521 udp_client
1704706 udp_client.cc
1704545 udp_server
1704719 udp_server.cc
1704720 udp_server.hpp

3,转发客户端消息的服务器

[user@iZwz9eoohx59fs5a6ampomZ udp]$ ll
total 132
prw-rw-r-- 1 user user     0 Oct  5 18:08 clientA
prw-rw-r-- 1 user user     0 Oct  5 18:08 clientB
-rw-rw-r-- 1 user user  1290 Oct  2 23:08 log.hpp
-rw-rw-r-- 1 user user   208 Oct  5 17:27 Makefile
-rw-rw-r-- 1 user user   858 Oct  5 17:10 thread.hpp
-rwxrwxr-x 1 user user 25304 Oct  5 17:31 udp_client
-rw-rw-r-- 1 user user  2897 Oct  5 17:14 udp_client.cc
-rwxrwxr-x 1 user user 75440 Oct  5 17:31 udp_server
-rw-rw-r-- 1 user user   488 Oct  5 17:31 udp_server.cc
-rw-rw-r-- 1 user user  5187 Oct  5 17:23 udp_server.hpp
[user@iZwz9eoohx59fs5a6ampomZ udp]$ cat udp_server.hpp 
#ifndef _UDP_SAERVAER_HPP
#define _UDP_SAERVAER_HPP

#include "log.hpp"
#include <unordered_map>
#include <cstdio>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <queue>

#define SIZE 1024

class UdpServer
{
public:
    // UdpServer(uint16_t port, std::string ip="0.0.0.0")
    UdpServer(uint16_t port, std::string ip= "")
    : _port(port),
      _ip(ip),
      _sock(-1)
    {}
    ~UdpServer()
    {
        if(_sock >= 0) close(_sock);
    }
    bool initServer()
    {
        // 从这里开始,就是新的系统调用,来完成网络功能啦
        // 1.创建套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0); //AF_INET和PF_INET是一样的
        if(_sock < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        // 2.设置好缓冲区,把ip和port从主机序列转换到网络序列
        // "192.168.1.3" -> 点分十进制字符串IP地址
        // 每一个区域的取值范围[0-255]: 1个字节 -> 4个区域
        // 理论上,标识一个IP地址,其实4个字节就够了
        // 点分十进制字符串风格的IP地址 -> 4个字节
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); //结构体清零
        local.sin_family = AF_INET;   //服务器的IP和端口未来也是要发送给对方主机的 -> 先要将数据发送到网络
        local.sin_port = htons(_port);//主机序列转成网络序列,规定大小端
        // 同上,先要将点分十进制字符串风格的IP地址 转成-> 4字节
        // 然后再把,4字节主机序列 转成-> 网络序列
        // inet_addr接口可以把 十进制字符串 转成-> 网络序列
        // 还能让服务器在工作的过程在,可以从任意IP中获取数据
        // INADDR_ANY宏就是IP 0.0.0.0
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        
        // 3.bind:将用户设置的ip和port在内核中,要和我们当前的进程强关联
        if(bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        // 4.初始化成功,提示信息
        logMessage(NORMAL, "init udp serverdone ... %s", strerror(errno));
        return true;
    }
    void Start()
    {
        // 作为一款网络服务器,是永远不退出的
        // 服务器启动 -> 常驻进程 -> 永远在内存中存在,除非挂了
        // echo服务器,客户端给服务端发消息,原封不动的返回消息即可
        
        // 1.设置缓冲区
        char buffer[SIZE];
        for(;;)
        {
            //peer为纯输出型参数
            struct sockaddr_in peer;
            bzero(&peer,sizeof(peer));
            //len = sizeof(peer)固定写法
            //len是输入输出型参数
            //输入是:peer的缓冲区大小
            //输出是:实际读到的peer
            socklen_t len = sizeof(peer);

            //2.读取数据
            char result[256];
            char key[64];
            std::string cmd_echo;
            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
            if(s > 0)//读取成功
            {
                //3.处理数据
                buffer[s] = 0; // 我们目前的数据当做字符串
                uint16_t cli_port = ntohs(peer.sin_port);      // 从网络中来的!
                std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节的网络序列的IP->本主机的字符串风格的IP,方便显示
                // printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);
                snprintf(key, sizeof(key), "%s-%u", cli_ip.c_str(), cli_port); // 127.0.0.1-8080
                logMessage(NORMAL, "key: %s", key);
                // std::string key_string = key;
                auto it = _users.find(key);
                if (it == _users.end())
                {
                    // exists
                    logMessage(NORMAL, "add new user : %s", key);
                    _users.insert({key, peer});
                }
            }
            
            //4.发送数据
            for (auto &iter : _users)
            {
                std::string sendMessage = key;
                sendMessage += "# ";
                sendMessage += buffer; // 127.0.0.1-1234# 你好
                logMessage(NORMAL, "push message to %s", iter.first.c_str());
                sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr *)&(iter.second), sizeof(iter.second));
            }
        }
    }
private:
    // 一个服务器,一般必须需要ip地址和port端口号
    std::string _ip; // ip地址
    uint16_t _port;  // port端口号
    int _sock;       // 通信套接字
    std::unordered_map<std::string, struct sockaddr_in> _users; //用户地址和数据包
    std::queue<std::string> messageQueue; //信息
};

#endif
[user@iZwz9eoohx59fs5a6ampomZ udp]$ cat udp_server.cc
#include "udp_server.hpp"
#include <memory>
#include <cstdlib>

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << "Set_Port\n" << std::endl;
}

int main(int argc, char* argv[])
{
    // if(argc != 3)
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    
    // std::string ip = argv[1];
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port));

    svr->initServer();
    svr->Start();

    return 0;
}
[user@iZwz9eoohx59fs5a6ampomZ udp]$ cat thread.hpp 
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cstdio>

// typedef std::function<void* (void*)> fun_t;
typedef void *(*fun_t)(void *);

class ThreadData
{
public:
    void *args_;
    std::string name_;
};

class Thread
{
public:
    Thread(int num, fun_t callback, void *args) : func_(callback)
    {
        char nameBuffer[64];
        snprintf(nameBuffer, sizeof nameBuffer, "Thread-%d", num);
        name_ = nameBuffer;

        tdata_.args_ = args;
        tdata_.name_ = name_;
    }
    ~Thread()
    {}
    void start()
    {
        pthread_create(&tid_, nullptr, func_, (void*)&tdata_);
    }
    void join()
    {
        pthread_join(tid_, nullptr);
    }
    std::string name()
    {
        return name_;
    }
private:
    std::string name_;
    fun_t func_;
    ThreadData tdata_;
    pthread_t tid_;
};
[user@iZwz9eoohx59fs5a6ampomZ udp]$ cat udp_client.cc 
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <memory>
#include "thread.hpp"

// 发现:无论是多线程读还是写,用的sock都是一个,sock代表就是文件,UDP是全双工的-> 可以同时进行收发而不受干扰

uint16_t serverport = 0;
std::string serverip;

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " serverIp serverPort\n" << std::endl;
}

static void *udpSend(void *args)
{
    int sock = *(int *)((ThreadData *)args)->args_;
    std::string name = ((ThreadData *)args)->name_;

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

    while (true)
    {
        std::cerr << "请输入你的信息# "; //标准错误 2打印
        std::getline(std::cin, message);
        if (message == "quit") break;
        // 当client首次发送消息给服务器的时候,OS会自动给client bind他的IP和PORT
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof server);
    }

    return nullptr;
}

static void *udpRecv(void *args)
{
    int sock = *(int *)((ThreadData *)args)->args_;
    std::string name = ((ThreadData *)args)->name_;

    char buffer[1024];
    while (true)
    {
        memset(buffer, 0, sizeof(buffer));
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout  << buffer << std::endl;
        }
    }
}

// ./udp_client 127.0.0.1 8080
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }

    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    serverport = atoi(argv[2]);
    serverip = argv[1];
    // client要不要bind??要,但是一般client不会显示的bind,程序员不会自己bind
    // client是一个客户端 -> 普通人下载安装启动使用的-> 如果程序员自己bind了->
    // client 一定bind了一个固定的ip和port,万一,其他的客户端提前占用了这个port呢??
    // client一般不需要显示的bind指定port,而是让OS自动随机选择(什么时候做的呢?)

    std::unique_ptr<Thread> sender(new Thread(1, udpSend, (void *)&sock));
    std::unique_ptr<Thread> recver(new Thread(2, udpRecv, (void *)&sock));

    // sender->name();
    sender->start();
    recver->start();

    sender->join();
    recver->join();

    close(sock);

    return 0;
}

// 服务器端  
[user@iZwz9eoohx59fs5a6ampomZ udp]$ ./udp_server 8080
[NORMAL] [1759658973] init udp serverdone ... Success
[NORMAL] [1759658996] key: 127.0.0.1-45893
[NORMAL] [1759658996] add new user : 127.0.0.1-45893
[NORMAL] [1759658996] push message to 127.0.0.1-45893
[NORMAL] [1759658998] key: 127.0.0.1-45893
[NORMAL] [1759658998] push message to 127.0.0.1-45893
[NORMAL] [1759659000] key: 127.0.0.1-45893
[NORMAL] [1759659000] push message to 127.0.0.1-45893
[NORMAL] [1759659021] key: 127.0.0.1-48021
[NORMAL] [1759659021] add new user : 127.0.0.1-48021
[NORMAL] [1759659021] push message to 127.0.0.1-48021
[NORMAL] [1759659021] push message to 127.0.0.1-45893
[NORMAL] [1759659022] key: 127.0.0.1-48021
[NORMAL] [1759659022] push message to 127.0.0.1-48021
[NORMAL] [1759659022] push message to 127.0.0.1-45893
[NORMAL] [1759659023] key: 127.0.0.1-48021
[NORMAL] [1759659023] push message to 127.0.0.1-48021
[NORMAL] [1759659023] push message to 127.0.0.1-45893
[NORMAL] [1759659024] key: 127.0.0.1-48021
[NORMAL] [1759659024] push message to 127.0.0.1-48021
[NORMAL] [1759659024] push message to 127.0.0.1-45893
^C

// 客户端A
[user@iZwz9eoohx59fs5a6ampomZ udp]$ ./udp_client 127.0.0.1 8080 > clientA
请输入你的信息# 你好
请输入你的信息# 哈哈哈
请输入你的信息# 66666
请输入你的信息# 

// 客户端B
[user@iZwz9eoohx59fs5a6ampomZ udp]$ ./udp_client 127.0.0.1 8080 > clientB
请输入你的信息# 6666
请输入你的信息# 77777
请输入你的信息# 8888
请输入你的信息# 9999
请输入你的信息# 

// 消息端
[user@iZwz9eoohx59fs5a6ampomZ udp]$ cat < clientA
127.0.0.1-45893# 你好
127.0.0.1-45893# 哈哈哈
127.0.0.1-45893# 66666
127.0.0.1-48021# 6666
127.0.0.1-48021# 77777
127.0.0.1-48021# 8888
127.0.0.1-48021# 9999

四、简单的TCP网络程序

1,单进程版的服务器

[user@iZwz9eoohx59fs5a6ampomZ tcp]$ cat tcp_server.hpp
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <pthread.h>
#include "log.hpp"

static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{
    //echo server
    char buffer[1024];
    while(true)
    {
        // read && write 可以直接使用
        ssize_t s = read(sock, buffer, sizeof(buffer)-1);
        if(s > 0)//链接成功
        {
            buffer[s] = 0;
            printf("%s:%d# %s\n", clientip.c_str(), clientport, buffer);
        }
        else if(s == 0)//对端关闭链接,客户端和服务端一起退出
        {
            logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
            break;
        }
        else//链接失败
        {
            logMessage(FATAL, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }

        write(sock, buffer, strlen(buffer));
    }
}

class TcpServer
{
public:
    TcpServer(uint16_t port,std::string ip = "")
    : _port(port),
      _ip(ip),
      _listensock(-1)
    {}
    ~TcpServer()
    {}
    void initServer()
    {
        // 1.创建套接字
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if(_listensock < 0)
        {
            logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create socket success , _listensock:%d", _listensock);
        
        // 2.设置缓冲区
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

        // 3.绑定进程
        if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }

        // 4.建立链接
        // tcp是面向连接的,正式通信的时候,需要先建立链接
        if(listen(_listensock, gbacklog) < 0)
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "listen socket success");
    }
    void start()
    {
        // 主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
        // signal(SIGCHLD, SIG_IGN);
        while(true)
        {
            // 5.获取连接
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            // fd(提供服务的李四王五) vs _sock(拉客的张三)
            int ServiceSock = accept(_listensock, (struct sockaddr*)&src, &len);
            if(ServiceSock < 0) // 获取连接失败
            {
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                exit(5);
            }
            // 获取连接成功,开始进行通信服务
            uint16_t client_port = ntohs(_port);
            std::string client_ip = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "link success, ServiceSock: %d | %s : %d |\n", ServiceSock, client_ip.c_str(), client_port);

            // 6.通信服务
            // 版本1 单进程版 -- 一次只能处理一个客户端,上一个处理完了才能处理下一个,显然不能直接使用
            service(ServiceSock, client_ip, client_port);
        }
    }
private:
    uint16_t _port;
    std::string _ip;
    // int _sock;
    int _listensock; // tcp使用的是监听套接字
};
[user@iZwz9eoohx59fs5a6ampomZ tcp]$ cat tcp_server.cc
#include "tcp_server.hpp"
#include <memory>
#include <cstdlib>

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << "Set_Port\n" << std::endl;
}

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<TcpServer> svr(new TcpServer(port));
    svr->initServer();
    svr->start();

    return 0;
}
[user@iZwz9eoohx59fs5a6ampomZ tcp]$ cat tcp_client.cc
#include <memory>
#include <cstdlib>
#include <iostream>
#include <cstring>
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << "Server_Ip Server_Port\n" << std::endl;
}

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }

    std::string ip = argv[1];
    uint16_t port = atoi(argv[2]);

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock < 0)
    {
        printf("socket error\n");
        exit(2);
    }

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

    if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0)
    {
        printf("socket error\n");
        exit(3);
    }
    printf("connect success\n");

    while(true)
    {
        std::string line;
        std::cout << "请输入要发送的消息# ";
        std::getline(std::cin, line);
        send(sock, line.c_str(), line.size(), 0);
        char buffer[1024];
        ssize_t s = recv(sock, buffer, sizeof(buffer)-1, 0);
        if(s > 0)
        {
            buffer[s] = 0;
            std::cout << "server return# " << buffer << std::endl;
        }
        else if(s == 0)
        {
            break;
        }
        else
        {
            break;
        }
    }

    close(sock);

    return 0;
}

[user@iZwz9eoohx59fs5a6ampomZ tcp]$ make clean;make
rm -f tcp_client tcp_server
g++ -o tcp_client tcp_client.cc -std=c++11 -lpthread
g++ -o tcp_server tcp_server.cc -std=c++11 -lpthread
[user@iZwz9eoohx59fs5a6ampomZ tcp]$ ./tcp_server 8080
[NORMAL] [1759757658] create socket success , _listensock:3
[NORMAL] [1759757658] listen socket success
[NORMAL] [1759757711] link success, ServiceSock: 4 | 127.0.0.1 : 36895 |

127.0.0.1:36895# 你哈
127.0.0.1:36895# 哈哈哈哈
127.0.0.1:36895# 吃了嘛
127.0.0.1:36895# 吃月饼了
[NORMAL] [1759757733] 127.0.0.1:36895 shutdown, me too!
^C

// 客户端
[user@iZwz9eoohx59fs5a6ampomZ tcp]$ ./tcp_client 127.0.0.1 8080
connect success
请输入要发送的消息# 你哈
server return# 你哈
请输入要发送的消息# 哈哈哈哈
server return# 哈哈哈哈
请输入要发送的消息# 吃了嘛
server return# 吃了嘛
请输入要发送的消息# 吃月饼了
server return# 吃月饼了
请输入要发送的消息# ^C

2,多进程版的服务器

[user@iZwz9eoohx59fs5a6ampomZ tcp]$ cat tcp_server.hpp 
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <pthread.h>
#include "log.hpp"

static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{
    //echo server
    char buffer[1024];
    while(true)
    {
        // read && write 可以直接使用
        ssize_t s = read(sock, buffer, sizeof(buffer)-1);
        if(s > 0)//链接成功
        {
            buffer[s] = 0;
            printf("%s:%d# %s\n", clientip.c_str(), clientport, buffer);
        }
        else if(s == 0)//对端关闭链接,客户端和服务端一起退出
        {
            logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
            break;
        }
        else//链接失败
        {
            logMessage(FATAL, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }

        write(sock, buffer, strlen(buffer));
    }
}
 
class TcpServer
{
public:
    TcpServer(uint16_t port,std::string ip = "")
    : _port(port),
      _ip(ip),
      _listensock(-1)
    {}
    ~TcpServer()
    {}
    void initServer()
    {
        // 1.创建套接字
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if(_listensock < 0)
        {
            logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create socket success , _listensock:%d", _listensock);
        
        // 2.设置缓冲区
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

        // 3.绑定进程
        if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }

        // 4.建立链接
        // tcp是面向连接的,正式通信的时候,需要先建立链接
        if(listen(_listensock, gbacklog) < 0)
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "listen socket success");
    }
    void start()
    {
        // 主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
        // signal(SIGCHLD, SIG_IGN);
        while(true)
        {
            // 5.获取连接
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            // fd(提供服务的李四王五) vs _sock(拉客的张三)
            int ServiceSock = accept(_listensock, (struct sockaddr*)&src, &len);
            if(ServiceSock < 0) // 获取连接失败
            {
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                exit(5);
            }
            // 获取连接成功,开始进行通信服务
            uint16_t client_port = ntohs(_port);
            std::string client_ip = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "link success, ServiceSock: %d | %s : %d |\n", ServiceSock, client_ip.c_str(), client_port);

            // 6.通信服务
            // 版本1 单进程版 -- 一次只能处理一个客户端,上一个处理完了才能处理下一个,显然不能直接使用
            // service(ServiceSock, client_ip, client_port);

            // 版本2 多进程版 -- 父进程阻塞等待监听socket,每接收到一个任务,就创建一个子进程去执行
            // pid_t id = fork();
            // assert(id != -1);
            // if(id == 0)
            // {
            //     // 子进程
            //     close(_listensock);
            //     service(ServiceSock, client_ip, client_port);//单进程通信
            //     exit(0);// 僵尸状态
            // }
            // 父进程
            // 父进程关闭ServiceSock,不会影响子进程,子进程会继承父进程的fd文件描述符,继续执行任务
            // 缺点:父进程会不断占用ServiceSock,让可用的文件描述符越来越少(上限大概在5万/10万),造成文件描述符泄露
            // close(ServiceSock);

            // 版本2.1 -- 多进程(孙进程)版 --不会造成文件描述符泄露
            pid_t id = fork();
            assert(id != -1);
            if(id == 0)
            {
                // 子进程
                close(_listensock);
                if(fork() > 0) exit(0); // 创建孙进程执行任务,子进程本身立马退出
                // 孙进程,是孤儿进程,被OS系统接管,通信任务结束,由OS自动回收资源
                service(ServiceSock, client_ip, client_port); // 单进程通信
                exit(0); // 通信任务结束,由OS自动回收资源
            }
            // 父进程
            // 子进程创建孙进程后立马退出,父进程不需要阻塞等待子进程返回
            waitpid(id, nullptr, 0);//父进程不会阻塞等待!
            close(ServiceSock);
        }
    }
private:
    uint16_t _port;
    std::string _ip;
    // int _sock;
    int _listensock; // tcp使用的是监听套接字
};
[user@iZwz9eoohx59fs5a6ampomZ tcp]$ 
rm -f tcp_client tcp_server
g++ -o tcp_client tcp_client.cc -std=c++11 -lpthread
g++ -o tcp_server tcp_server.cc -std=c++11 -lpthread
[user@iZwz9eoohx59fs5a6ampomZ tcp]$ ./tcp_server 8080
[NORMAL] [1759761399] create socket success , _listensock:3
[NORMAL] [1759761399] listen socket success
[NORMAL] [1759761410] link success, ServiceSock: 4 | 127.0.0.1 : 36895 |

127.0.0.1:36895# 你好
[NORMAL] [1759761424] link success, ServiceSock: 4 | 127.0.0.1 : 36895 |

127.0.0.1:36895# 吃月饼了嘛
127.0.0.1:36895# 吃了
127.0.0.1:36895# 什么味的
127.0.0.1:36895# 豆沙味的
[NORMAL] [1759761461] 127.0.0.1:36895 shutdown, me too!
[NORMAL] [1759761462] 127.0.0.1:36895 shutdown, me too!
^C

//客户端
[user@iZwz9eoohx59fs5a6ampomZ tcp]$ ./tcp_client  127.0.0.1 8080
connect success
请输入要发送的消息# 吃月饼了嘛
server return# 吃月饼了嘛
请输入要发送的消息# 什么味的
server return# 什么味的
请输入要发送的消息# ^C

[user@iZwz9eoohx59fs5a6ampomZ tcp]$ ./tcp_client 127.0.0.1 8080
connect success
请输入要发送的消息# 你好
server return# 你好
请输入要发送的消息# 吃了
server return# 吃了
请输入要发送的消息# 豆沙味的
server return# 豆沙味的
请输入要发送的消息# ^C

3,多线程版的服务器

[user@iZwz9eoohx59fs5a6ampomZ tcp]$ ./tcp_server 8080
[NORMAL] [1759763022] create socket success , _listensock:3
[NORMAL] [1759763022] listen socket success
[NORMAL] [1759763033] link success, ServiceSock: 4 | 127.0.0.1 : 36895 |

[NORMAL] [1759763052] link success, ServiceSock: 5 | 127.0.0.1 : 36895 |

127.0.0.1:36895# 你好
127.0.0.1:36895# 吃了吗
127.0.0.1:36895# 吃了月饼
127.0.0.1:36895# 啥味的?
127.0.0.1:36895# 莲蓉味的
[NORMAL] [1759763170] 127.0.0.1:36895 shutdown, me too!
[NORMAL] [1759763171] 127.0.0.1:36895 shutdown, me too!
^C

// 客户端
[user@iZwz9eoohx59fs5a6ampomZ tcp]$ ./tcp_client 127.0.0.1 8080
connect success
请输入要发送的消息# 你好   
server return# 你好
请输入要发送的消息# 吃了吗
server return# 吃了吗
请输入要发送的消息# 啥味的?    
server return# 啥味的?
请输入要发送的消息# ^C
[user@iZwz9eoohx59fs5a6ampomZ tcp]$ ./tcp_client 127.0.0.1 8080
connect success
请输入要发送的消息# 吃了月饼
server return# 吃了月饼
请输入要发送的消息# 莲蓉味的
server return# 莲蓉味的
请输入要发送的消息# ^C

4,线程池版的服务器

[user@iZwz9eoohx59fs5a6ampomZ tcp]$ tree
.
├── makefile
├── tcp_client
├── tcp_client.cc
├── tcp_server
├── tcp_server.cc
├── tcp_server.hpp
└── ThreadPool
    ├── lockGuard.hpp
    ├── log.hpp
    ├── Task.hpp
    ├── thread.hpp
    └── threadPool.hpp

1 directory, 11 files
[user@iZwz9eoohx59fs5a6ampomZ tcp]$ cat tcp_server.hpp 
#pragma once

#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <ctype.h>
#include <signal.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>

#include "ThreadPool/log.hpp"
#include "ThreadPool/threadPool.hpp"
#include "ThreadPool/Task.hpp"

// 一直发送消息的服务
static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{
    //echo server
    char buffer[1024];
    while(true)
    {
        // read && write 可以直接使用
        ssize_t s = read(sock, buffer, sizeof(buffer)-1);
        if(s > 0)//链接成功
        {
            buffer[s] = 0;
            printf("%s:%d# %s\n", clientip.c_str(), clientport, buffer);
        }
        else if(s == 0)//对端关闭链接,客户端和服务端一起退出
        {
            logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
            break;
        }
        else//链接失败
        {
            logMessage(FATAL, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }
        write(sock, buffer, strlen(buffer));
    }
    close(sock);
}

// 发送单条消息的服务
static void Service_2(int sock, const std::string &clientip, const uint16_t &clientport, const std::string &thread_name)
{
    // read && write 可以直接使用
    char buffer[1024];
    ssize_t s = read(sock, buffer, sizeof(buffer)-1);
    if(s > 0)//链接成功
    {
        buffer[s] = 0;
        printf("%s:%d# %s\n", clientip.c_str(), clientport, buffer);
        std::string message;
        char* start = buffer;
        write(sock, buffer, strlen(buffer));
    }
    else if(s == 0)//对端关闭链接,客户端和服务端一起退出
    {
        logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
    }
    else//链接失败
    {
        logMessage(FATAL, "read socket error, %d:%s", errno, strerror(errno));
    }
    close(sock);
}

// 大小写转化服务
static void Service_3(int sock, const std::string &clientip, const uint16_t &clientport, const std::string &thread_name)
{
    // read && write 可以直接使用
    char buffer[1024];
    ssize_t s = read(sock, buffer, sizeof(buffer)-1);
    if(s > 0)// 链接成功
    {
        buffer[s] = 0; // 将发过来的数据当成字符串
        std::cout << thread_name << " | " << clientip << " : " << clientport << "# ";
        std::string message;
        char* str = buffer;
        printf("%s\n", str);
        while(*str)
        {
            // 使用标准库函数tolower和toupper
            *str = isupper(*str) ? tolower(*str) : toupper(*str); 
            message.push_back(*str);
            str++;
        }
        write(sock, message.c_str(), message.size());
    }
    else if(s == 0) // 对端关闭链接,客户端和服务端一起退出
    {
        logMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
    }
    else // 链接失败
    {
        logMessage(FATAL, "read socket error, %d:%s", errno, strerror(errno));
    }
    close(sock);
}

// 在 版本3 多线程版 使用
// class ThreadData
// {
// public:
//     int _sock;
//     std::string _ip;
//     uint16_t _port;
// };

class TcpServer
{
private:
    // 在 版本3 多线程版 使用
    // static void* threadRoutine(void* args)
    // {
    //     // 问题:不需要pthread_join()线程等待的时候,如何回收资源?
    //     // 要调用pthread_detach线程分离,自动回收资源,不然会造成内存泄漏
    //     pthread_detach(pthread_self());
    //     ThreadData *td = static_cast<ThreadData*>(args);
    //     service(td->_sock, td->_ip, td->_port);
    //     delete td;
    //     return nullptr;
    // }
public:
    TcpServer(uint16_t port,std::string ip = "")
    : _port(port),
      _ip(ip),
      _listensock(-1),
      _threadpool_ptr(ThreadPool<Task>::getThreadPool())
    {}
    ~TcpServer()
    {}
    void initServer()
    {
        // 1.创建套接字
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if(_listensock < 0)
        {
            logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create socket success , _listensock:%d", _listensock);
        
        // 2.设置缓冲区
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

        // 3.绑定进程
        if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }

        // 4.建立链接
        // tcp是面向连接的,正式通信的时候,需要先建立链接
        if(listen(_listensock, gbacklog) < 0)
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "listen socket success");
    }
    void start()
    {
        // 在 版本2 多进程版 使用
        // 主动忽略SIGCHLD信号,让子进程退出的时候,会自动释放自己的僵尸状态
        // signal(SIGCHLD, SIG_IGN); 

        // 在 版本4 线程池版 使用
        _threadpool_ptr->run();

        while(true)
        {
            // 5.获取连接
            struct sockaddr_in src;
            socklen_t len = sizeof(src);
            // ServiceSock(提供服务的李四王五) vs _listensock(拉客的张三)
            int ServiceSock = accept(_listensock, (struct sockaddr*)&src, &len);
            if(ServiceSock < 0) // 获取连接失败
            {
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                exit(5);
            }

            // 6.获取连接成功,把ip和port转换成主机序列
            uint16_t client_port = ntohs(_port);
            std::string client_ip = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "link success, ServiceSock: %d | %s : %d |\n", ServiceSock, client_ip.c_str(), client_port);

            // 7.通信服务
            // 版本1 单进程版 -- 一次只能处理一个客户端,上一个处理完了才能处理下一个,显然不能直接使用
            // service(ServiceSock, client_ip, client_port);

            // 版本2 多进程版 -- 父进程阻塞等待监听socket,每接收到一个任务,就创建一个子进程去执行
            // pid_t id = fork();
            // assert(id != -1);
            // if(id == 0)
            // {
            //     // 子进程
            //     close(_listensock);
            //     service(ServiceSock, client_ip, client_port);//单进程通信
            //     exit(0);// 僵尸状态
            // }
            // 父进程
            // 父进程关闭ServiceSock,不会影响子进程,子进程会继承父进程的fd文件描述符,继续执行任务
            // 缺点:父进程会不断占用ServiceSock,让可用的文件描述符越来越少(上限大概在5万/10万),造成文件描述符泄露
            // close(ServiceSock);

            // 版本2.1 -- 多进程(孙进程)版 --不会造成文件描述符泄露
            // pid_t id = fork();
            // assert(id != -1);
            // if(id == 0)
            // {
            //     // 子进程
            //     close(_listensock);
            //     if(fork() > 0) exit(0); // 创建孙进程执行任务,子进程本身立马退出
            //     // 孙进程,是孤儿进程,被OS系统接管,通信任务结束,由OS自动回收资源
            //     service(ServiceSock, client_ip, client_port); // 单进程通信
            //     exit(0); // 通信任务结束,由OS自动回收资源
            // }
            // 父进程
            // 子进程创建孙进程后立马退出,父进程不需要阻塞等待子进程返回
            // waitpid(id, nullptr, 0);//父进程不会阻塞等待!
            // close(ServiceSock);

            // 版本3 -- 多线程版
            // ThreadData *td = new ThreadData();
            // td->_sock = ServiceSock;
            // td->_ip = client_ip;
            // td->_port = client_port;
            // pthread_t tid;
            // 多线程不要关闭文件描述符,新线程和主线程共享文件描述符表
            // pthread_create(&tid, nullptr, threadRoutine, td);
            
            // 版本4 -- 线程池版本
            Task t(ServiceSock, client_ip, client_port, Service_3);
            assert(_threadpool_ptr != nullptr);
            _threadpool_ptr->pushTask(t);
        }
    }
private:
    uint16_t _port;  //设置服务器网址
    std::string _ip; //设置服务器端口
    // int _sock;    //udp写法    
    int _listensock; // tcp使用的是监听套接字
    const static int gbacklog = 20; // gbacklog连接队列总容量 = accept已连接数 + SYN待连接数
    std::unique_ptr<ThreadPool<Task>> _threadpool_ptr; // 线程池指针
};
[user@iZwz9eoohx59fs5a6ampomZ tcp]$ cat tcp_server.cc
#include "tcp_server.hpp"
#include <memory>
#include <cstdlib>

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << "Set_Port\n" << std::endl;
}

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<TcpServer> svr(new TcpServer(port));
    svr->initServer();
    svr->start();

    return 0;
}
[user@iZwz9eoohx59fs5a6ampomZ tcp]$ cat tcp_client.cc
#include <memory>
#include <cstdlib>
#include <iostream>
#include <cstring>
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << "Server_Ip Server_Port\n"
              << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }

    std::string ip = argv[1];
    uint16_t port = atoi(argv[2]);

    // int sock = socket(AF_INET, SOCK_STREAM, 0);
    // if(sock < 0)
    // {
    //     printf("socket error\n");
    //     exit(2);
    // }

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

    // if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0)
    // {
    //     printf("socket error\n");
    //     exit(3);
    // }
    // printf("connect success\n");

    // 单次请求连接 -- 连接成功后循环发消息
    // while(true)
    // {
    //     std::string line;
    //     std::cout << "请输入要发送的消息# ";
    //     std::getline(std::cin, line);
    //     send(sock, line.c_str(), line.size(), 0);
    //     char buffer[1024];
    //     ssize_t s = recv(sock, buffer, sizeof(buffer)-1, 0);
    //     if(s > 0)
    //     {
    //         buffer[s] = 0;
    //         std::cout << "server return# " << buffer << std::endl;
    //     }
    //     else if(s == 0)
    //     {
    //         break;
    //     }
    //     else
    //     {
    //         break;
    //     }
    // }
    // close(sock);

    // 循环请求连接 -- 模拟多人连接服务器
    bool alive = false;
    int sock;
    std::string line;
    while (true)
    {
        if (!alive)
        {
            sock = socket(AF_INET, SOCK_STREAM, 0);
            if (sock < 0)
            {
                printf("socket error\n");
                exit(2);
            }

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

            if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0)
            {
                printf("socket error\n");
                exit(3);
            }

            printf("connect success\n");
            alive = true;

            std::cout << "请输入要发送的消息# ";
            std::getline(std::cin, line);
            if (line == "quit")
                break;
        }

        ssize_t sen = send(sock, line.c_str(), line.size(), 0);
        if (sen > 0)
        {
            char buffer[1024];
            ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
            if (s > 0)
            {
                buffer[s] = 0;
                std::cout << "server return# " << buffer << std::endl;
            }
            else if (s == 0)
            {
                alive = false;
                close(sock);
            }
        }
        else
        {
            alive = false;
            close(sock);
        }
    }

    return 0;
}
[user@iZwz9eoohx59fs5a6ampomZ tcp]$ cat ThreadPool/Task.hpp 
#pragma once

#include <iostream>
#include <string>
#include <functional>
#include "log.hpp"

// 回调函数类型
// typedef std::function<void (int , const std::string &, const uint16_t &, const std::string &)> func_t;
using func_t = std::function<void ( int, const std::string &, const uint16_t &, const std::string &)>;

class Task
{
public:
    Task(){}
    Task(int sock, const std::string ip, uint16_t port, func_t func)
    : _sock(sock), _ip(ip), _port(port), _func(func)
    {}
    // 执行任务函数封装
    void execute(const std::string &name)
    {
        // 执行任务回调函数
        _func(_sock, _ip, _port, name); 
    }
public:
    int _sock;
    std::string _ip;
    uint16_t _port;
    func_t _func; //执行任务回调函数
};
[user@iZwz9eoohx59fs5a6ampomZ tcp]$ make clean;make
rm -f tcp_client tcp_server
g++ -o tcp_client tcp_client.cc -std=c++11 -lpthread
g++ -o tcp_server tcp_server.cc -std=c++11 -lpthread
[user@iZwz9eoohx59fs5a6ampomZ tcp]$ ./tcp_server 8080
[NORMAL] [1760365934] create socket success , _listensock:3
[NORMAL] [1760365934] listen socket success
[NORMAL] [1760365934] Thread-1 启动成功
[NORMAL] [1760365934] Thread-2 启动成功
[NORMAL] [1760365934] Thread-3 启动成功
[NORMAL] [1760365934] Thread-4 启动成功
[NORMAL] [1760365934] Thread-5 启动成功
[NORMAL] [1760365934] Thread-6 启动成功
[NORMAL] [1760365934] Thread-7 启动成功
[NORMAL] [1760365934] Thread-8 启动成功
[NORMAL] [1760365934] Thread-9 启动成功
[NORMAL] [1760365934] Thread-10 启动成功
[NORMAL] [1760365955] link success, ServiceSock: 4 | 127.0.0.1 : 36895 |

Thread-1 | 127.0.0.1 : 36895# aaaaa
[NORMAL] [1760365957] link success, ServiceSock: 5 | 127.0.0.1 : 36895 |

Thread-2 | 127.0.0.1 : 36895# sssss
[NORMAL] [1760365959] link success, ServiceSock: 4 | 127.0.0.1 : 36895 |

Thread-3 | 127.0.0.1 : 36895# awegaerheah
[NORMAL] [1760365960] link success, ServiceSock: 5 | 127.0.0.1 : 36895 |

Thread-4 | 127.0.0.1 : 36895# yjdtjsrahras
[NORMAL] [1760365962] link success, ServiceSock: 4 | 127.0.0.1 : 36895 |

Thread-5 | 127.0.0.1 : 36895# SARGRAH
[NORMAL] [1760365964] link success, ServiceSock: 5 | 127.0.0.1 : 36895 |

Thread-6 | 127.0.0.1 : 36895# YMDJF
[NORMAL] [1760365966] link success, ServiceSock: 4 | 127.0.0.1 : 36895 |

Thread-7 | 127.0.0.1 : 36895# rgraeWSEGsrg
[NORMAL] [1760365969] link success, ServiceSock: 5 | 127.0.0.1 : 36895 |

Thread-8 | 127.0.0.1 : 36895# SGASERGsegesag
[NORMAL] [1760365972] link success, ServiceSock: 4 | 127.0.0.1 : 36895 |

Thread-9 | 127.0.0.1 : 36895# afaewNGFDNT
[NORMAL] [1760365976] link success, ServiceSock: 5 | 127.0.0.1 : 36895 |

Thread-10 | 127.0.0.1 : 36895# gYJByjgYGyjGUK
[NORMAL] [1760365979] link success, ServiceSock: 4 | 127.0.0.1 : 36895 |

[NORMAL] [1760365982] 127.0.0.1:36895 shutdown, me too!
^C

// 客户端
[user@iZwz9eoohx59fs5a6ampomZ tcp]$ ./tcp_client 127.0.0.1 8080
connect success
请输入要发送的消息# aaaaa
server return# AAAAA
connect success
请输入要发送的消息# sssss
server return# SSSSS
connect success
请输入要发送的消息# awegaerheah
server return# AWEGAERHEAH
connect success
请输入要发送的消息# yjdtjsrahras
server return# YJDTJSRAHRAS
connect success
请输入要发送的消息# SARGRAH
server return# sargrah
connect success
请输入要发送的消息# YMDJF
server return# ymdjf
connect success
请输入要发送的消息# rgraeWSEGsrg
server return# RGRAEwsegSRG
connect success
请输入要发送的消息# SGASERGsegesag
server return# sgasergSEGESAG
connect success
请输入要发送的消息# afaewNGFDNT
server return# AFAEWngfdnt
connect success
请输入要发送的消息# gYJByjgYGyjGUK
server return# GyjbYJGygYJguk
connect success
请输入要发送的消息# ^C

五、TCP协议通讯流程

下图是基于TCP协议的客户端/服务器程序的一般流程:

1,服务器初始化:

调用socket, 创建文件描述符;

调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失 败;

调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备; 调用accecpt, 并阻塞, 等待客户端连接过来;

2,建立连接的过程:

调用socket, 创建文件描述符;

调用connect, 向服务器发起连接请求;

connect会发出SYN段并阻塞等待服务器应答; (第一次)

服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)

客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)

这个建立连接的过程, 通常称为三次握手

3,数据传输的过程:

建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方 可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;

服务器从accept()返回后立刻调用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;

这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期 间客户端调用read()阻塞等待服务器的应答;

服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;

客户端收到后从read()返回, 发送下一条请求,如此循环下去;

4,断开连接的过程:

如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次); 此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);

read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送 一个FIN; (第三次)

客户端收到FIN, 再返回一个ACK给服务器; (第四次)

这个断开连接的过程, 通常称为四次挥手

5,应用程序和TCP协议层的交互过程

在学习socket API时要注意应用程序和TCP协议层是如何交互的。

应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段。

应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段。

6,TCP 和 UDP 对比

可靠传输vs 不可靠传输

有连接 vs 无连接

字节流 vs 数据报

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值