套接字学习

学习目标

认识网络字节序等网络编程中的基本概念

学习socket api的基本用法

能够实现一个简单的udp客户端/服务器

能够实现一个简单的tcp客户端/服务器(单连接版本, 多进程版本, 多线程版本)

理解tcp服务器建立连接, 发送数据, 断开连接的流程

一、网络字节序

网络数据流有大端小端之分

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

1.1 简述TCP/IP协议

TCP/IP协议规定网络数据流应采用大端字节序,即低地址高字节. 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据; 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

1. 2 网络字节序和主机字节序的转换

当我们进行网络编程时,需要将主机字节序的数据转换为网络字节序,反之亦然。为了解决这个问题,我们通常使用一些专门的函数进行字节序的转换。这些函数包括:

  1. htonl:将一个无符号长整形数从主机字节序转换为网络字节序。

  2. ntohl:将一个无符号长整形数从网络字节序转换为主机字节序。

  3. htons:将一个无符号短整形数从主机字节序转换为网络字节序。

  4. ntohs:将一个无符号短整形数从网络字节序转换为主机字节序。

这些函数的名字中的 "h" 表示 "host","n" 表示 "network","s" 表示 "short","l" 表示 "long"。例如,htonl 的全名是 "host to network long",意思是将一个长整形数从主机字节序转换为网络字节序。

二、socket api

(1) socket

这个函数用于创建一个新的套接字。它需要三个参数:协议族(例如,AF_INET 表示 IPv4,AF_INET6 表示 IPv6),套接字类型(例如,SOCK_STREAM 表示 TCP,SOCK_DGRAM 表示 UDP),以及协议(通常设置为0,表示采用默认协议)。如果成功,这个函数返回一个可以用于通信的套接字描述符

(2) bind

这个函数用于将套接字与特定的 IP 地址和端口号绑定。它需要三个参数:一个套接字描述符,一个指向 struct sockaddr 的指针(这个结构包含了你想要的 IP 地址和端口号),以及这个结构的大小。通常在服务器端使用。

1)struct sockaddr

struct sockaddr 是一个通用的套接字地址结构,用于保存网络地址信息。这个结构在 <sys/socket.h> 头文件中定义。它被设计为一个通用的、可以处理任何类型的地址的结构,包括但不限于 IP 地址。

image-20230729103926538

下面是这个结构在大多数系统中的定义:

struct sockaddr {
    unsigned short sa_family;  // address family, AF_xxx
    char sa_data[14];          // 14 bytes of protocol address
};

sa_family 字段表示地址的类型,例如 AF_INET 表示 IPv4 地址,AF_INET6 表示 IPv6 地址。这个字段告诉系统如何解析 sa_data 字段中的内容。

sa_data 字段包含了实际的地址信息,但是因为它是一个字符数组,所以不能直接用于访问 IP 地址或者端口号。

因为 struct sockaddr 的通用性,一般我们在实际编程中不会直接使用它,而是使用特定协议的地址结构,例如 struct sockaddr_in(用于 IPv4),然后在需要 struct sockaddr 作为参数的函数调用中进行类型转换。

(3) recvfrom

这个函数用于接收数据,通常用于 UDP 协议。它的参数包括一个套接字描述符,一个缓冲区用于存储接收的数据,缓冲区的大小,一些标志,一个指向 struct sockaddr 的指针用于存储发送者的地址,以及一个指向地址的长度的指针。

(4) sendto

这个函数用于发送数据,通常用于 UDP 协议。它的参数包括一个套接字描述符,一个包含要发送数据的缓冲区,缓冲区的大小,一些标志,一个指向 struct sockaddr 的指针包含了接收者的地址,以及这个地址的长度。

(5) listen

这个函数用于使套接字进入被动监听状态,等待连接请求,通常用于 TCP 协议。它需要两个参数:一个套接字描述符,以及一个整数表示在处理前可以排队等待的最大连接数。通常在服务器端使用。

(6) connect

这个函数用于发起到另一个套接字的连接,通常用于 TCP 协议。它需要三个参数:一个套接字描述符,一个指向 struct sockaddr 的指针(这个结构包含了你想要连接的目标 IP 地址和端口号),以及这个结构的大小。通常在客户端使用。

(7) accept

这个函数用于接受连接请求并返回一个新的套接字描述符,这个新的描述符对应的是与客户端之间的连接,通常用于 TCP 协议。它需要三个参数:一个套接字描述符,一个指向 struct sockaddr 的指针用于存储连接的客户端的地址,以及一个指向地址的长度的指针。通常在服务器端使用。

三、UDP协议

传输层协议

无连接

不可靠传输

面向数据报

3.1 模拟实现简单UDP

这个程序可以接收来自多个客户端的消息,并将每个消息转发给所有已知的客户端。每次有新的客户端发送消息时,它都会被添加到服务器的已知客户端列表中

3.1.1 日志信息
#pragma once
#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3
​
const char *log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};
​
#define LOGFILE "server.log"
​
class Log
{
public:
    Log():logFd(-1)
    {}
    void enable()
    {
        umask(0);
        logFd = open(LOGFILE, O_WRONLY | O_CREAT | O_APPEND, 0666);
        assert(logFd != -1);
        dup2(logFd, 1);
        dup2(logFd, 2);
    }
    ~Log()
    {
        if(logFd != -1) 
        {
            fsync(logFd);
            close(logFd);
        }
    }
private:
    int logFd;
};
​
// logMessage(DEBUG, "%d", 10);
void logMessage(int level, const char *format, ...)
{
    assert(level >= DEBUG);
    assert(level <= FATAL);
​
    char *name = getenv("USER");
​
    char logInfo[1024];
    va_list ap; // ap -> char*
    va_start(ap, format);
    vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);
    va_end(ap); // ap = NULL
    FILE *out = (level == FATAL) ? stderr : stdout;
    fprintf(out, "%s | %u | %s | %s\n",
            log_level[level],
            (unsigned int)time(nullptr),
            name == nullptr ? "unknow" : name,
            logInfo);
​
    fflush(out); // 将C缓冲区中的数据刷新到OS
    fsync(fileno(out));   // 将OS中的数据尽快刷盘
}

3.1.2 服务端
//用于显示如何使用该程序。用户应当至少提供端口号,可以选择性地提供 IP 地址。
static void Usage(const std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " port [ip]" << std::endl;
}
​
​
class udpServer
{
public:
    //构造函数用于初始化服务器的 IP 地址、端口号和套接字描述符。
    udpServer(int port,std::string ip="")
    :_port((uint16_t)port)
    ,_ip(ip)
    ,_sockfd(-1){}
    ~udpServer(){}
    //创建一个 UDP 套接字,并绑定给定的 IP 和端口。
    void init()
    {
        //1 创建套接字
        //udp是采用IPv4,无连接方式通信
        //IPv4:使用 32 位地址空间来标识互联网上的设备。由四个十进制数(每个数范围从 0 到 255)组成,例如 192.168.0.1
        _sockfd=socket(AF_INET,SOCK_DGRAM,0);
        //如果失败返回-1
        if(_sockfd==-1)
        {
            logMessage(FATAL,"%s:%d",strerror(errno),_sockfd);
            exit(-1);
        }
        logMessage(DEBUG,"socket create sucessed:%d",_sockfd);
        //2 网络信息填充及绑定
        //2.1 添加网络信息(ip、port)
        //sockaddr_in网络通信 sockaddr_un本地通信
        struct sockaddr_in local;
        bzero(&local,sizeof(local));
        //地址族(Address Family),指示地址的类型,常见的值包括 AF_INET(IPv4)、AF_INET6(IPv6)
        local.sin_family=AF_INET;
        //从主机字节序转换为网络字节序
        local.sin_port=htons(_port);
        //INADDR_ANY,表示绑定到任意可用的 IP 地址。这通常用于服务器程序,表示接受来自任意 IP 地址的连接。
        //自动进行从主机字节序转换为网络字节序
        local.sin_addr.s_addr=_ip.empty()?INADDR_ANY:inet_addr(_ip.c_str());
        //2.2 绑定网络信息
        //sockaddr_in--->sockaddr根据地址簇进行区分
        if(bind(_sockfd,(const sockaddr *)&local,sizeof(local))==-1)
        {
            logMessage(FATAL,"bind:%s:%d",strerror(errno),_sockfd);
            exit(-2);
        }
        logMessage(DEBUG, "socket bind success: %d", _sockfd);
    }
    
    
    //这是服务器的主要循环,用于接收消息,记录发送消息的客户端,并将消息路由给所有已知的客户端。
    void start()
    {
        //输入缓冲区
        char bufferIn[1024];
        //输出缓冲区
        char bufferOut[1024];
        //死循环
        while(true)
        {
            //记录远程传输端的ip、port
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            bzero(&peer,sizeof(peer));
            //接受信息
            ssize_t s=recvfrom(_sockfd,bufferIn,sizeof(bufferIn)-1,0,(sockaddr*)&peer,&len);
            if(s>0)
            {
                bufferIn[s]='\0';
            }
            uint16_t peerPort=ntohs(peer.sin_port);
            std::string peerIp=inet_ntoa(peer.sin_addr);
            //读取成功
            logMessage(NOTICE, "[%s:%d]# %s", peerIp.c_str(), peerPort, bufferIn);
            //检查在线成员,有就添加
            checkOnlineUser(peerIp, peerPort, peer);
            //消息路由,给所有在线成员发送消息
            messageRoute(peerIp, peerPort,bufferIn);
        }
    }
private:
    //检查发送消息的客户端是否已知。如果不是,则将其添加到已知客户端列表中。
    void checkOnlineUser(std::string peerIp, uint16_t peerPort,struct sockaddr_in peer)
    {
        std::string key=peerIp;
        key+=":";
        key+=std::to_string(peerPort);
        //查询在线成员
        auto iter =_users.find(key);
        if(iter == _users.end())
        {
            _users.insert({key, peer});
        }
    }
    
    
    //将收到的消息路由给所有已知的客户端。
    void messageRoute(std::string ip, uint16_t port, std::string info)
    {
​
        std::string message = "[";
        message += ip;
        message += ":";
        message += std::to_string(port);
        message += "]# ";
        message += info;
        for(auto &user : _users)
        {
            sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&(user.second), sizeof(user.second));
        }
    }
private:
    //端口号
    uint16_t _port;
    //ip地址
    std::string _ip;
    //套接字
    int _sockfd;
    std::unordered_map<std::string,sockaddr_in> _users;
};
​
​
int main(int argc,char *argv[])
{
    if (argc != 2 && argc != 3) 
    {
        Usage(argv[0]);
        exit(3);
    }
    uint16_t port = atoi(argv[1]);
    std::string ip;
    if (argc == 3)
    {
        ip = argv[2];
    }
    Log enable();
    udpServer svr(port, ip);
    svr.init();
    svr.start();
    return 0;
}
3.1.3客户端
static void Usage(const std::string proc)
{
    std::cout << "Usage:\n\t" << proc << "serverIp serverPort" << std::endl;
}
​
​
void *recverAndPrint(void *args)
{
    while (true)
    {
        int sockfd = *(int *)args;
        char buffer[1024];
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }
}
​
​
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(3);
    }
    // 1 设置将要访问的服务器ip、port
    uint16_t serverPort = atoi(argv[2]);
    std::string serverIp = argv[1];
    // 2 创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1)
    {
        logMessage(FATAL, "%s:%d", strerror(errno), sockfd);
        exit(-1);
    }
    logMessage(DEBUG, "socket create sucessed:%d", sockfd);
    // 3 添加网络信息(ip、port)
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverPort);
    server.sin_addr.s_addr = inet_addr(serverIp.c_str());
    //多线程用于收发消息
    pthread_t t;
    pthread_create(&t, nullptr, recverAndPrint, (void *)&sockfd);
    // 4 通讯过程
    std::string buffer;
    while (true)
    {
        std::cerr << "Please Enter# ";
        std::getline(std::cin, buffer);
        // 发送消息给server
        //客户端在sendto过程中,自动绑定网络信息,由操作系统维护
        sendto(sockfd,buffer.c_str(),buffer.size(),0,(const sockaddr*)&server,sizeof(server));
    }
    close(sockfd);
    return 0;
}

3.2 常用的测试指令

3.2.1 netstat

netstat 命令用于显示网络连接、路由表、接口统计等信息。

netstat -lnup 命令的具体作用是显示与UDP相关的正在监听的套接字及其关联的进程。让我们解释这些选项:

  • -l: 仅显示正在监听的套接字。

  • -n: 显示数值地址,而不是解析主机名。

  • -u: 显示UDP连接。

  • -p: 显示每个套接字所属的进程ID和程序名称。

注意本地环回ip:127.0.0.1 表示本主机通信

3.2.2 ps

ps axj:

  • a: 显示所有进程(包括所有用户的进程)。

  • x: 显示没有控制终端的进程。例如后台进程。

  • j: 显示扩展格式的信息,包括进程组 ID (PGID)、会话 ID (SID)、控制终端 (TTY),以及进程与会话的关联状态。

使用 axj 选项可以得到一个详细的进程列表,其中包含了与进程相关的会话和进程组信息。

ps -aL:

  • -a: 显示所有进程(包括其他用户的进程)。

  • -L: 显示所有的线程,而不仅仅是进程。在现代操作系统中,一个进程可以包含多个执行线程。

使用 -aL 选项可以查看所有进程的所有线程。

四、TCP协议

传输层协议

有连接

可靠传输

面向字节流

4.1 模拟实现简单TCP

这个程序可以接收来自多个客户端的任务,并将任务添加到线程池中处理。

4.1.1 任务
#include <iostream>
#include <string>
#include <functional>
class Task
{
private:
    //std::function 是一个类模板,它为任何可调用对象(包括函数、lambda、函数对象和成员函数指针)提供了一个通用的类型。
    using callback_t=std::function<void(int,uint16_t,std::string)>;//typedef std::function<void(int,uint16_t,std::string)> callback_t;
public:
    Task():_sock(-1), _port(-1){}
    Task(int sock, uint16_t port, std::string ip, callback_t func)
    :_sock(sock),  _port(port), _ip(ip),_func(func)
    {}
    ~Task(){}
    void operator () ()
    {
        logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 开始啦...",pthread_self(), _ip.c_str(), _port);
        _func(_sock, _port, _ip);
        logMessage(DEBUG, "线程ID[%p]处理%s:%d的请求 结束啦...",pthread_self(), _ip.c_str(), _port);
    }
private:
    int _sock;
    uint16_t _port;
    std::string _ip;
    callback_t _func;
};

4.1.2服务端
#include "util.hpp"
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
​
class ServerTcp
{
public:
    ServerTcp(uint16_t port, const std::string &ip = "") : port_(port), ip_(ip), listenSock_(-1),_tp(nullptr)
    {}
    ~ServerTcp()
    {}
​
public:
    void init()
    {
        // 1 创建socket
        /*
            PF_INET和AF_INET相同
            SOCK_STREAM是由于TCP面向字节流
            注意此时返回的是监听套接字,并不提供服务
        */
        listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
        if (listenSock_ < 0)
        {
            logMessage(FATAL, "socket: %s", strerror(errno));
            exit(SOCKET_ERR);
        }
        logMessage(DEBUG, "socket: %s, %d", strerror(errno), listenSock_);
​
        // 2. 网络信息填充及绑定
        // 2.1 填充服务器信息
        struct sockaddr_in local; // 用户栈
        memset(&local, 0, sizeof local);
        local.sin_family = PF_INET;
        local.sin_port = htons(port_);
        ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
        // 2.2 绑定服务器信息,从用户栈写入内核
        if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
        {
            logMessage(FATAL, "bind: %s", strerror(errno));
            exit(BIND_ERR);
        }
        logMessage(DEBUG, "bind: %s, %d", strerror(errno), listenSock_);
​
        // 3  监听socket
        if (listen(listenSock_, 5) < 0)
        {
            logMessage(FATAL, "listen: %s", strerror(errno));
            exit(LISTEN_ERR);
        }
        logMessage(DEBUG, "listen: %s, %d", strerror(errno), listenSock_);
        
        // 创建线程池
        _tp=ThreadPool<Task>::getInstance();
    }
​
    void loop()
    {
        // 运行线程池
        _tp->start();
        while (true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 4 获取连接 
            /*
                此时获得的文件描述符代表提供服务的套接字
                如果获取链接时退出,那么服务器将无法重启
                并且注意服务结束后要关闭这个文件描述符,否则会造成内存泄漏
            */
            int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
            if (serviceSock < 0)
            {
                // 获取链接失败
                logMessage(WARINING, "accept: %s[%d]", strerror(errno), serviceSock);
                continue;
            }
            // 4.1 获取客户端基本信息
            uint16_t peerPort = ntohs(peer.sin_port);
            std::string peerIp = inet_ntoa(peer.sin_addr);
​
            logMessage(DEBUG, "accept: %s | %s[%d], socket fd: %d",
                       strerror(errno), peerIp.c_str(), peerPort, serviceSock);
            //创建任务
            /*
            bind 用于绑定参数并生成一个新的可调用对象,可以使用占位符(std::_1, std::_2, ...)来表示参数,允许稍后在调用绑定的函数时提供这些参数
                */
            Task t(serviceSock,peerPort, peerIp,
                   std::bind(&ServerTcp::transService,this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
            //添加到任务队列
            //注意线实际上线程池不适合死循环任务,而是短任务,否则会长时间占用线程
            _tp->push(t);
        }
    }
    
    
    // 大小写转化服务
    void transService(int sock,  uint16_t clientPort,const std::string &clientIp)
    {
        assert(sock >= 0);
        assert(!clientIp.empty());
        assert(clientPort >= 1024);
        char inbuffer[BUFFER_SIZE];
        while (true)
        {
            ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1); //我们认为我们读到的都是字符串
            if (s > 0)
            {
                inbuffer[s] = '\0';
                if(strcasecmp(inbuffer, "quit") == 0)
                {
                    logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
                    break;
                }
                logMessage(DEBUG, "trans before: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer);
                // 大小写转化
                for(int i = 0; i < s; i++)
                {
                    if(isalpha(inbuffer[i]) && islower(inbuffer[i])) 
                        inbuffer[i] = toupper(inbuffer[i]);
                }
                logMessage(DEBUG, "trans after: %s[%d]>>> %s", clientIp.c_str(), clientPort, inbuffer);
​
                write(sock, inbuffer, strlen(inbuffer));
            }
            else if (s == 0)
            {
                //写端退出
                logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
                break;
            }
            else
            {
                logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno));
                break;
            }
        }
        close(sock); // 关闭服务套接字,防止文件描述符泄漏
        logMessage(DEBUG, "server close %d done", sock);
    }
    
    
    //执行命令并返回执行信息
    void execCommand(int sock, const std::string &clientIp, uint16_t clientPort)
    {
        assert(sock >= 0);
        assert(!clientIp.empty());
        assert(clientPort >= 1024);
        
        char command[BUFFER_SIZE];
        while (true)
        {
            ssize_t s = read(sock, command, sizeof(command) - 1); 
            if (s > 0)
            {
                command[s] = '\0';
                logMessage(DEBUG, "[%s:%d] exec [%s]", clientIp.c_str(), clientPort, command);
                // 考虑安全
                std::string safe = command;
                if((std::string::npos != safe.find("rm")) || (std::string::npos != safe.find("unlink")))
                {
                    break;
                }
                /*
popen() 用于从一个进程中创建另一个进程并与其通信的库函数。它提供了一种方法来执行shell命令并读取或写入到该进程的标准输入/输出。该函数返回一个文件指针,可以用于读取子进程的输出或向其写入输入,并且在完成读/写操作后,使用 `pclose()` 来关闭文件指针。
FILE *popen(const char *command, const char *type);
                */
                FILE *fp = popen(command, "w");
                if(fp == nullptr)
                {
                    logMessage(WARINING, "exec %s failed, beacuse: %s", command, strerror(errno));
                    break;
                }
                dup2(sock, fp->_fileno);
                fflush(fp);
                pclose(fp);
                logMessage(DEBUG, "[%s:%d] exec [%s] ... done", clientIp.c_str(), clientPort, command);
            }
            else if (s == 0)
            {
                logMessage(DEBUG, "client quit -- %s[%d]", clientIp.c_str(), clientPort);
                break;
            }
            else
            {
                logMessage(DEBUG, "%s[%d] - read: %s", clientIp.c_str(), clientPort, strerror(errno));
                break;
            }
        }
​
        close(sock); 
        pclose(fp);
        logMessage(DEBUG, "server close %d done", sock);
    }
private:
    int listenSock_;
    uint16_t port_;
    std::string ip_;
    ThreadPool<Task>* _tp;
};
​
static void Usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " port ip" << std::endl;
    std::cerr << "example:\n\t" << proc << " 8080 127.0.0.1\n" << std::endl;
​
}
​
​
int main(int argc, char *argv[])
{
    if(argc != 2 && argc != 3 )
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);
    std::string ip;
    if(argc == 3) ip = argv[2];
    Log enable();
    daemonize();
    ServerTcp svr(port, ip);
    svr.init();
    svr.loop();
    return 0;
}
4.1.3 客户端
#include "util.hpp"
​
volatile bool quit = false;
​
static void Usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " serverIp serverPort" << std::endl;
    std::cerr << "Example:\n\t" << proc << " 127.0.0.1 8081\n"
              << std::endl;
}
​
class clientTcp
{
public:
    clientTcp(uint16_t port, const std::string &ip) : _serverPort(port), _serverIp(ip), _sock(-1){}
    ~clientTcp(){}
public:
    void init()
    {
        // 1 创建套接字
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if (_sock < 0)
        {
            std::cerr << "socket: " << strerror(errno) << std::endl;
            exit(SOCKET_ERR);
        }
​
        // 2 发起链接请求
        // 2.1 填充远端服务器信息
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(_serverPort);
        inet_aton(_serverIp.c_str(), &server.sin_addr);
        // 2.2 发起请求,并自动绑定
        if (connect(_sock, (const struct sockaddr *)&server, sizeof(server)) != 0)
        {
            std::cerr << "connect: " << strerror(errno) << std::endl;
            exit(CONN_ERR);
        }
        std::cout << "info : connect success: " << _sock << std::endl;
    }
    void loop()
    {
        std::string message;
        while (!quit)
        {
            message.clear();
            std::cout << "请输入你的消息>>> ";
            std::getline(std::cin, message);
            if (strcasecmp(message.c_str(), "quit") == 0)
                quit = true;
​
            ssize_t s = write(_sock, message.c_str(), message.size());
            if (s > 0)
            {
                message.resize(1024);
                ssize_t s = read(_sock, (char *)(message.c_str()), 1024);
                if (s > 0)
                    message[s] = 0;
                std::cout << "Server Echo>>> " << message << std::endl;
            }
            else if (s <= 0)
            {
                break;
            }
        }
        close(_sock);
    }
private:
    int _sock;
    uint16_t _serverPort;
    std::string _serverIp;
};
​
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    std::string serverIp = argv[1];
    uint16_t serverPort = atoi(argv[2]);
    clientTcp client(serverPort,serverIp);
    client.init();
    client.loop();
    return 0;
}
4.1.4 守护进程(精灵进程)

守护进程(精灵进程)是后台进程,独立于控制终端并周期性地执行某些任务或等待处理某些事件,不受登录登出的影响。通常用于服务器程序,如邮件服务器、系统日志服务器或任何其他需要在后台运行的程序。

(1)会话

会话是一个或多个进程组的集合且每个会话都有一个唯一的会话 ID。

会话开始于用户登录并持续到注销为止。通常每个终端窗口都是一个独立的会话。

会话的领头进程是创建该会话的进程。当你在终端中登录时,shell 通常是那个会话的领头进程

一个会话只能有一个领头进程。

当会话领头进程终止时,该会话中的所有进程都会收到 SIGHUP 信号。

在给定的会话中,任何时候都只能有一个进程组在前台运行。这意味着它可以从终端接收输入而其他的进程组在后台运行。

通常,会话的创建与控制终端的分离/获取有关。例如,守护进程通常会调用 setsid() 系统调用来开始一个新的会话并摆脱任何控制终端。

(2)创建守护进程(daemonize)
  1. 创建子进程并结束父进程: 这样可以确保进程不是进程组长,从而使得进程能成功调用setsid()。

  2. 在子进程中创建一个新会话: 使用setsid()创建新会话,使进程成为新会话的领头进程。这样可以确保进程不会再有控制终端。

  3. 改变当前工作目录: 通常会将当前工作目录更改为根目录,以确保进程不会阻止文件系统被卸载。

  4. 重设文件权限掩码: 这通常是为了确保进程可以读写其创建的任何文件。

  5. 关闭文件描述符: 进程从其父进程那里继承了文件描述符。不需要这些文件描述符,因此它们应该被关闭。

  6. 处理信号: 进程应该正确处理它可能收到的信号,忽略SIGPIPE。

void daemonize()
{
    int fd = 0;
    // 1 忽略SIGHUP
    signal(SIGHUP, SIG_IGN);
    // 2 让自己不要成为进程组组长
    if (fork() > 0)
        exit(0);
    // 3 设置自己是一个独立的会话
    setsid();
    // 4 更改进程的工作目录到根目录
    chdir("/");
    // 5 重设文件权限掩码
    umask(0);
    // 6 重定向0,1,2,"/dev/null"为文件黑洞,读不返回,写丢弃
    if ((fd = open("/dev/null", O_RDWR)) != -1) // fd == 3
    {
        dup2(fd, STDIN_FILENO);
        dup2(fd, STDOUT_FILENO);
        dup2(fd, STDERR_FILENO);
        // 7 关闭掉不需要的fd
        if(fd > STDERR_FILENO) close(fd);
    }
}
(3)三种守护进程方案
1)传统模式
2)使用 daemon() 函数

在某些系统中,有一个 daemon() 函数可以用来创建守护进程。基本上封装了上述的传统方法的大部分步骤。

3)使用 systemd 或其他现代初始化系统

现代 Linux 系统通常使用 systemd 作为其初始化系统。在 systemd 中,你可以创建一个服务单元文件来描述你的守护进程,并告诉 systemd 如何启动和管理它。当 systemd 启动服务时,它会处理创建守护进程所需的大部分步骤。使用这种方法,你可以避免手动编写守护进程的代码,并利用 systemd 提供的许多其他功能,如日志记录、资源限制和依赖管理。

4.2 TCP协议通信流程

TCP(Transmission Control Protocol,传输控制协议)是一个面向连接、可靠的、基于字节流的通信协议。它确保数据在发送者和接收者之间正确无误地传输。下面是 TCP 通信的基本流程:

image-20230818212304613

4.2.1连接建立(三次握手):

SYN: 客户端发送一个 SYN(synchronize)包给服务器,以请求建立连接。这个包中会包含一个客户端的初始序列号。

SYN-ACK: 服务器收到 SYN 包后,会确认请求并回应一个 SYN-ACK(synchronize-acknowledge)包。这个包中会包含服务器的初始序列号和客户端初始序列号的确认。

ACK: 客户端收到 SYN-ACK 包后,发送一个 ACK(acknowledge)包给服务器,确认它已收到服务器的初始序列号。此时,连接建立完成。

4.2.2 数据传输:

一旦连接建立,双方都可以开始发送数据。TCP 通过序列号和确认号确保数据的可靠传输。

如果数据包在网络中丢失或到达时出错,接收方不会确认那个数据包,发送方在超时后会重新发送那个数据包。

TCP 也提供流控制功能,这可以防止一方发送的数据速度超过另一方的接收能力。

4.2.3 连接终止(四次挥手):

FIN: 当一方完成数据发送后,它会发送一个 FIN(finish)包给另一方,请求关闭连接。

ACK: 另一方会确认这个 FIN 包。

FIN: 当另一方也完成数据发送后,它也会发送一个 FIN 包。

ACK: 第一方确认这个 FIN 包。此时,连接被完全关闭。

4.2.4错误处理和流控制:

TCP 提供了多种错误处理和流控制机制,如超时重传、滑动窗口、拥塞控制等,以确保数据的可靠传输。

整个 TCP 通信流程被设计得非常可靠,确保在不可靠的 IP 网络上提供稳定的、按顺序的、无差错的数据传输服务。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值