UDP&TCP网络编程

udp编程接口

一个UDP程序的编写可以分为3步:

  • 创建一个网络套接字:

    它相当于文件操作时的文件描述符,是一个程序进行网络通讯的门户, 所有的网络操作都要基于它

  • 绑定IP和端口:

    需要为网络套接字填充IP和端口信息

    但是有些时候无需手动填充,让系统自动自动分配即可

  • 发送和接收消息

    • 发送消息需要指明对方的IP和端口号
    • 接收消息不需要,直接从套接字拿就行

socket

申请一个套接字

套接字:相当于一个文件描述符,其中存放着IP、端口、网络协议等信息;所有的网络操作都要基于这个网络套接字,就像所有文件操作都要基于文件描述符一样

函数原型及参数解析

#include <sys/socket.h>
#include <sys/types.h> 

int socket(int domain, int type, int protocol);
  • domain:socket的域;选择本地通讯网络通信

    AF_UNIX(AF_LOCAL):本地通讯

    AF_INET:IPv4协议网络通讯

    AF_INET6:IPv6协议网络通讯

  • type:套接字的类型;决定通信时对应的报文;udp–>用户数据报,tcp–>流式

    SOCK_STREAM:流式–>tcp

    SOCK_DGRAM:数据报格式,无链接,不可靠–>udp

  • protocol:协议类型;网络应用中一般用 0

  • 返回值:返回一个文件描述符

Example

#include <sys/socket.h>
#include <sys/types.h> 
int main()
{
    int sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd_ < 0)
    {
        exit(1);
    }
}

bind

绑定网络信息

将网络信息写入网络套接字对应的内核区域

函数原型及参数解析

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>//struct sockaddr结构体定义
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:网络套接字, 表示将网络信息绑定到这个套接字

  • addr:要进行绑定的网络信息(IP、端口号)

    我们要用一个结构体存储存储网络信息,然后把结构体传入bind函数,用于绑定

    由于socket创建的套接字需要兼容本地、网络等多个域,多个协议,而这些协议需要绑定的信息也不尽相同,对应描述信息的的结构体就不同,如:

    • 对于网络信息的描述就要有IP端口号网络通讯协议(下面的struct sockaddr_in结构体)
    • 本地信息的描述就要有路径名和本地的各种通讯协议(下面的struct sockaddr_un结构体)

    image-20221203161834884

    我们可以用一种多态的理念直接给bind函数传入两种类型结构体变量的首地址,当函数内要获取网络信息的时候,先读前16位知道当前要绑定信息的域和协议

    进而再对后面的位进行特定化读取

    这个addr参数完全可以用一个void*来接收两种不同的结构体指针,但是由于一些历史原因,当时还没有void*的语法

    所以,函数编写者新定义了一个结构体 struct sockaddr

    用法也很简单,只需要把struct sockaddr_in*struct sockaddr_un*强转为 struct sockaddr*传入即可,

    bind函数内部会自动通过通过前16位判断要选择哪种数据类型的绑定

    image-20221128153006434

    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 结构:

    struct sockaddr_in
    {
       __SOCKADDR_COMMON (sin_);   //16位地址类型,此句相当于unsigned short sin_family;
       in_port_t sin_port;			//端口号
       struct in_addr sin_addr;	//IP地址
    
       /* Pad to size of `struct sockaddr'.  */
       unsigned char sin_zero[8];
    };
    struct in_addr
    {
       unsigned short s_addr;//16位IP地址
    };
    
  • addrlen

    addr结构体变量的大小

Example:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
int main()
{
    // 创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd_ < 0)
    {
        exit(1);
    }
    // 填充网络信息结构体
    string ip = "127.0.0.1";
    uint16_t port = 8080; 
    struct sockaddr_in local;
    bzero(&local, sizeof(local));  // 初始化为全零
    local.sin_family = AF_INET;    // 填充协议家族,域,与创建套接字时的domain相同
    local.sin_port = htons(port);  // 填充端口号信息
    local.sin_addr.s_addr = ip.empty() ? htons(INADDR_ANY) : inet_addr(ip.c_str());// 填充IP信息
    // 绑定
    if (bind(sockfd, (sockaddr *)&local, sizeof(local)) < 0)
    {
        exit(1);
    }
}

INADDR_ANY:

程序员一般不用关心bind到哪个ip,

INADDR_ANY的值为0,传入的四字节IP如果是INADDR_ANY,则表示让编译器自动选择IP,进行绑定

一般指定填充一个确定的ip,在有特殊用途,或者测试时使用

云服务器上禁止bind的任何确定IP,只能使用 INADDR_ANY

recvfrom

从网络套接字中接收消息

函数原型及参数解析

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
  • sockfd:网络套接字

  • buf:读取到的目标缓冲区,读取长度len

  • flags:等待消息的方式(0–>阻塞式等待)

  • src_addr:发送方的网络信息会被填入其中(输出型参数)

  • 对方网络信息结构体的大小(输入、输出型参数,带入结构体大小,用于说明要为src_addr结构体开辟空间的大小,带出收到结构体大小)

    注意:接收消息时,无需告知发送方的地址,此结构体无需填充,消息会被发送方主动发送过来,通过套接字直接拿取即可

  • 返回值:返回-1表示读取出错

Example

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
int main()
{
    int sockfd;
    //生成套接字并完成绑定
    //...
    //开始接收消息
    char inbuff[1024];
    struct sockaddr_in peer;//用于存放消息发送方的网络信息
    socklen_t len = sizeof(peer);
    size_t s = recvfrom(sockfd,inbuff,sizeof(inbuff)-1,0,(sockaddr *)&peer, &len);
    if (s > 0)
    {
        inbuff[s] = 0;
    }
    else if (s == -1)
    {
        exit(1);
    }
    else;
    //读取成功,读到了对方的数据和网络地址【IP:port】
    string ip = inet_ntoa(peer.sin_addr);  //拿到对方的IP
    uint16_t port = ntohs(peer.sin_port);  //拿到对方的port
    //打印客户端发过来的消息和网络地址
    printf("[%s:%d]# %s", ip.c_str(), port, inbuff);
    return 0;
}

这个程序的功能就是从套接字读取一串字符并打印到屏幕

sendto

发送一条消息

函数原型及参数解析

#include <sys/types.h>
#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

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:发送目标的网络信息,

    注意:发送消息一定要通过此结构体,为sendto()提供发送目标的网络信息

  • addrlen:dest_addr结构体的大小

Example

int main(int argc, char const *argv[])
{
    //获取服务器IP,端口
    string server_ip = argv[1];
    uint16_t server_port = atoi(argv[2]);
    //创建客户端
    int socketfd = socket(AF_INET, SOCK_DGRAM, 0);
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    string buffer;
    cout << "请输入:";
    getline(cin, buffer);

    //发送消息
    sendto(socketfd, buffer.c_str(), buffer.size(), 0,
           (struct sockaddr *)&server, sizeof(server));
    return 0;
}

IP和port的格式转换

网络字节序<–>本地字节序

​ 由于不同操作系统,不同编译器有不同的字节序,为了网络通信的方便,网络字节序统一规定为大端,

​ 所有要进行网络传输的数据都要先转为网路字节序,

​ 从网络种接收到的数据也要先转为本地字节序

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);   //32位数转为网络字节序

uint16_t htons(uint16_t hostshort);  //16位数转为网络字节序

uint32_t ntohl(uint32_t netlong);    //32位数转为本地字节序

uint16_t ntohs(uint16_t netshort);   //16位数转为本地字节序

函数名解析:

​ < h: host --> 本地

​ n: network --> 网络

​ l: long --> 32位数

​ s:short --> 16位数 >

点分十进制IP<–>四字节二进制IP

服务器的IP地址我们一般写为:

"xx.xxx.xx.xxx"的点分十进制格式

但是这样的字符串实际不利于存储和计算机运算,所有结构体中存储的IP地址要以位段的方式用一个4字节数表示

如下这些函数用于将点分十进制的IP地址4字节IP互相转换

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
//点分十进制字符串-->4字节IP
int inet_aton(const char *cp, struct in_addr *inp); 
// 转换成功返回1,失败返回0,网络字节序ip自动写入in_addr结构体,认为255.255.255.255是有效IP,推荐使用
in_addr_t inet_addr(const char *cp);                
// 返回的4字节数是网络字节序,认为255.255.255.255是无效IP,返回-1
in_addr_t inet_network(const char *cp);             
// 返回的4字节数是本地字节序,认为255.255.255.255是无效IP,返回-1

//4字节IP-->点分十进制字符串
char *inet_ntoa(struct in_addr in);

UDP聊天室编写

一个聊天室需要有服务器端和客户端

服务器端负责接收消息,并将收得的消息发送给所有的已登陆用户

客户端负责发送消息,同时接收服务器同步过来的消息

用户登陆方式为:在客户端向服务器发送一条消息

如果用户长时间没有在聊天室发言,将会被提出群聊

日志打印

log.hpp

用于日志信息的打印

#pragma once
#include <stdlib.h>
#include <cassert>
#include <cstdio>
#include <ctime>
//日志等级
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3

const char* log_leval[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};
void logMssage(int level, const char* format, ...)
{
    assert(level >= DEBUG);
    assert(level <= FATAL);
    const char* name = getenv("USER");
    char logInfo[1024];
    va_list ap;
    va_start(ap, format);  //让dp对应到可变部分(...)
    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_leval[level],
            (unsigned)time(nullptr), name == nullptr ? "nukonw" : name,
            logInfo);
}

服务器端

udpServer.cc

#include <arpa/inet.h>
#include <cctype>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <netinet/in.h>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <unordered_map>

#include "log.hpp"
using namespace std;

class UdpServer
{
    struct Client
    {
        struct sockaddr_in peer;
        time_t time; // 到time之后如果没有更新过,就清除此用户
    };

private:
    // 服务器的socket fd信息
    int sockfd_;
    // 服务器的端口号信息
    uint16_t port_;
    // 服务器IP地址,点分十进制
    std::string ip_;
    // 在线用户
    std::unordered_map<std::string, struct Client> users_;
    // 超过此时间未响应将被踢出群聊(秒)
    const int tickOutTime_; 

public:
    UdpServer(int port, const string ip = "", int tickOutTime = 1000)
        : sockfd_(-1), // 初始化为-1,如果init创建失败,用-1表示失败
          port_(port),
          ip_(ip),
          tickOutTime_(tickOutTime)
    {
    }
    ~UdpServer() {}

public:
    void init()//创建套接字并绑定
    {
        // 1.创建socked套接字
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);  //相当于打开了一个文件
        if (sockfd_ < 0)
        {
            logMssage(FATAL, "%s:%d", strerror(errno), sockfd_);
            exit(1);
        }
        logMssage(DEBUG, "socket create success:%d", sockfd_);
        // 2. 绑定网络信息,ip+port
        // 2.1 先填充基本信息到 stckaddr_in
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        //填充协议家族,域
        local.sin_family = AF_INET;
        //填充端口号信息(htons():转为网络字节序)
        local.sin_port = htons(port_);
        //服务器都必须有IP地址,"xx.xxx.xx.xxx",
        // inet_addr():字符串风格点分十进制-->4字节IP-->uint32_t ip(位段方式),
        // 该函数会自动转网络字节序
        // INADDR_ANY(0):程序员不关心bind到哪个ip,让编译器自动绑定
        // inet_addr:指定填充一个确定的ip,特殊用途,或者测试时使用
        //禁止bind云服务器上的任何确定IP,只能使用 INADDR_ANY
        local.sin_addr.s_addr =
            ip_.empty() ? htons(INADDR_ANY) : inet_addr(ip_.c_str());
        // 2.2绑定
        if (bind(sockfd_, (sockaddr *)&local, sizeof(local)) < 0)
        {
            logMssage(FATAL, "%s:%d", strerror(errno), sockfd_);
            exit(2);
        }
    }
    void start()
    {
        while (true)
        {
            // demo2
            char inbuff[1024];
            char outbuff[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            size_t s = recvfrom(sockfd_, inbuff, sizeof(inbuff) - 1, 0,
                                (sockaddr *)&peer, &len);
            if (s > 0)
            {
                //当作字符串看待
                inbuff[s] = '\0';  
                outbuff[s] = '\0';
            }
            else if (s == -1)
            {
                logMssage(WARINING, "recvfrom:%s:%d", strerror(errno), sockfd_);
                continue;
            }
            //读取成功,读到了对方的数据和网络地址【IP:port】
            string peerIP = inet_ntoa(peer.sin_addr);
            uint16_t peerPort = ntohs(peer.sin_port);                                     // 拿到对方的port
            checkOnlineUser(peerIP, peerPort, {peer, (time_t)time(NULL) + tickOutTime_}); // 如果用户不存在则添加用户,存在则更新时间

            // 打印客户端发过来的消息和网络地址
            logMssage(NOTICE, "[%s:%d]# %s", peerIP.c_str(), peerPort, inbuff);
            messageRoute(peerIP, peerPort, inbuff); // 消息路由(将消息转发给除自己外的所有人)
        }
    }

private:
    // 如果用户不存在则添加用户,存在则更新时间
    void checkOnlineUser(string IP, uint16_t port, const Client &usr)
    {
        std::string key = IP;
        key += ":";
        key += std::to_string(port);
        if (users_.count(key))
        {
            users_[key].time = usr.time; // 更新时间
        }
        else
        {
            users_.insert({key, usr}); // 添加用户
        }
    }
    // 消息路由(将消息转发给除自己外的所有人)
   void messageRoute(string IP, uint16_t port, string message)
    {
        std::string from = IP;
        from += ":";
        from += std::to_string(port);
        string out = "[" + from + "]: " + message;
        // 记录超时未相应,退出的用户
        auto it = users_.begin();
        while (it != users_.end())
        {
            auto next = it; // 防止当前节点删除导致迭代器失效
            next++;
            if (it->first != from) // 发给自己外的所有人
            {
                if (time(NULL) <= it->second.time)
                {
                    sendto(sockfd_, out.c_str(), out.size(), 0, (sockaddr *)&it->second.peer, sizeof(it->second.peer));
                }
                else // 用户长时间没有动态将被踢出群聊
                {
                    // 发送退出消息
                    char exitMessage[] = "\1";
                    sendto(sockfd_, exitMessage, strlen(exitMessage), 0, (sockaddr *)&it->second.peer, sizeof(it->second.peer));
                    auto next = it;
                    users_.erase(it);
                    // exits.push_back(it);
                }
            }
            it = next;
        }
    }
};
static void Usage(const std::string porc)
{
    std::cout << "Usage:\n\t" << porc << " port [IP]" << std::endl;
}
// 程序运行方式:
// ./udpServer port IP
int main(int argc, char const *argv[])
{
    //确保命令行参数使用正确
    if (argc != 2 && argc != 3)
    {
        Usage(argv[0]);
        exit(3);
    }
    //端口号一定要有
    uint16_t port = atoi(argv[1]);
    //IP可以没有
    string ip;
    if (argc == 3)
    {
        ip = argv[2];
    }
    //网络服务器
    UdpServer svr(port, ip);
    //配置服务器网络信息
    svr.init();
    //开始运行服务器
    svr.start();
    return 0;
}

客户端

udpClient.cc

#include <arpa/inet.h>
#include <cstring>
#include <iostream>
#include <netinet/in.h>
#include <pthread.h>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

using namespace std;

// 用于接收消息和打印的线程
void *recvAndPrint(void *args)
{
    char bufferIn[1024]; // 消息接收的缓冲区
    while (true)
    {
        int sockfd = *(int *)args;
        // 从服务器接收消息
        struct sockaddr_in temp;
        // temp无需填充,作为输出型参数,带出服务器网络信息
        socklen_t len = sizeof(temp);
        // 接收消息不需要提供目的地址(网络信息),消息会被目标主动发送到本地套接字
        size_t s = recvfrom(sockfd, bufferIn, sizeof(bufferIn) - 1, 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            bufferIn[s] = 0;
            if (bufferIn[0] == '\1')
            {
                cout << "\r长时间未响应,你已退出群聊\n";
                exit(0);
            }
            cout << "\rserver echo# " << bufferIn << endl;
            cout << "请输入:";
        }
    }
}

static void Usage(const std::string porc)
{
    std::cout << "Usage:\n\t" << porc << "IP port" << std::endl;
}

int main(int argc, char const *argv[])
{
    // 必须有传入IP 和 端口号
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    // 1.创建客户端
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    // 不需要手动绑定,让系统自动为客户端分配IP和端口

    // 2.通讯过程
    // 2.1创建线程,循环从网络套接字接收消息
    pthread_t t;
    pthread_create(&t, NULL, recvAndPrint, &sockfd);

    // 2.2发送消息
    // 配置服务器的网络信息——发送消息的目的地
    // 从命令行参数获取服务器IP,端口
    string server_ip = argv[1];
    uint16_t server_port = atoi(argv[2]);
    // 填写服务器的网络信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());
    // 发送内容的缓冲区
    string bufferOut;
    // 循环读取内容并发送给服务器
    while (true)
    {
        cout << "请输入:";
        getline(cin, bufferOut);
        //发送消息给server
        sendto(sockfd, bufferOut.c_str(), bufferOut.size(), 0,
               (struct sockaddr *)&server, sizeof(server));
    }
    return 0;
}

makefile

.PHONY:all
all:udpClient udpServer

udpClient:udpClient.cc
	g++ -o $@ $^ -std=c++11 -lpthread
udpServer:udpServer.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f udpClient udpServer

tcp编程接口

tcp编程的前两步也依然是

  1. 创建socket套接字
  2. 绑定网络信息

与udp的区别在于:

创建socket的时候type选择SOCK_STREAM,的流式套接字

如下为tcp独有的部分:

  1. 设置为监听状态(listen)

    此步骤不一定要进行,只有被连接一方(服务器)需要设为监听

  2. 获取连接(accept)/发起连接(connect)

    一般由客户端发起连接(客户端知道服务器的IP和port),服务器获取连接

  3. 进行发消息(write)和读消息(read)

一般程序分为服务器端和客户端,编写步骤如下:

服务器端:

创建套接字
(socket)
绑定服务器信息
(bind)
设置监听状态
(listen)
获取连接
(accept)
发送/接收消息
(write/read)

客户端:

创建套接字
(socket)
发起连接请求
(accept)
发送/接收消息
(write/read)

listen

将socket套接字设为监听状态

因为tcp是面向面向连接的,所以要把当前套接字设为可连接的状态

函数原型及参数解析

#include <sys/types.h>          
#include <sys/socket.h>

int listen(int sockfd, int backlog);
  • sockfd:网络套接字
  • backlog:完成lisen后,当对端向本地发起connect,操作系统会自动完成连接,此连接被放入一个队列,当本地调用accept,就会从此队列中拿取连接状态,backlog即表示对列的最大容量,当超过此容量,操作系统即不会与对端进行连接(后续TCP原理时会详细讲解)
  • 返回值:成功返回0,失败返回-1,同时设置errno

example

如下是从创建套接字到设置监听状态的三个步骤:

int main()
{
    // 创建套接字
    int listenSock_ = socket(AF_INET, SOCK_STREAM, 0);
    if (listenSock_ < 0)
    {
        exit(1);
    }

    // bind
    // 2.1填充服务器信息
    uint16_t port_ = 8080;
    string ip_;
    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)) == -1)
    {
        exit(2);
    }

    // 3.监听socket,因为tcp是面向连接的,所以要把自己设为可连接的状态
    if (listen(listenSock_, 5) < 0)
    {
        exit(3);
    }
}

accept

让处于监听状态的套接字获取连接,此时如果对端发起connect即可完成连接。

函数原型及参数解析

#include <sys/types.h>  
#include <sys/socket.h>	

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd:监听套接字;这个socket绑定了IP和port,且设置了监听状态,通过它即可获取远端发起的连接
  • addr:发起连接一方的网络信息会被填入其中(输出型参数)
  • addrlen:传入addr对应结构体的大小,对端返回的结构体大小会被这个参数带出(输入输出型参数)
  • 返回值:返回一个新的文件描述符(套接字),后续的读写操作都要通过这个文件描述符进行

两个套接字的区别:

  • 传入的套接字是监听套接字,一般只有一个,这个套接字设置了监听状态,专门用于accept各个客户端发来的connect请求,让本地和对端建立连接
  • 返回的套接字可以认为是建立连接后,和远端对话的接口,如果有N个客户端和本地建立连接,则有N的服务套接字生成

example

如下代码续接listen的example:

struct sockaddr_in peer;
socklen_t size = sizeof(peer);
int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
if (serviceSock < 0)
{
    // 获取连接失败
    cerr << "accept error";
}

connect

向服务器发起连接

函数原型及参数解析

#include <sys/types.h> 
#include <sys/socket.h>

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

一般由客户端发起连接,创建套接字之后无需手动绑定无需设置监听状态,直接向远端发起connect,即可和远端服务器建立连接,在此过程,系统会自动为这个套接字绑定IP和端口,同时自己的网络信息也会被发送到远端。

  • sockfd:网络套接字
  • addr:对端的网络信息结构体,需要提前创建此结构体并填充IP,port,协议等信息
  • addr对应结构体的大小
  • 返回值:成功返回0;失败返回-1,并设置errno

example

int main()
{
    // 1.创建客户端套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    // ?2.不需要手动绑定,让系统自动为客户端分配IP和端口
    // ?3.不需要listen
    // 2.connect,向服务器发起连接请求
    std::string server_ip = "127.0.0.1";
    uint16_t server_port = atoi(8080);
    // 2.1 先填充远端的主机信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;                    // 协议
    server.sin_port = htons(server_port);           // port
    inet_aton(server_ip.c_str(), &server.sin_addr); // ip
    // 2.2发起请求,connect会自动选择合适的IP和port进行bind
    if (connect(sockfd, (const struct sockaddr *)&server, sizeof(server)) != 0)
    {
        std::cerr << "connect: " << strerror(errno);
        exit(CONN_ERR);
    }
}

read

接收网络消息与文件读取用的是同一个函数

函数原型及参数解析

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
  • fd:如果是读取本地的文件,fd是open()打开文件后生成的文件描述符;
    如果是接收网络消息,fd则是网络套接字,注意这个套接字不能是监听套接字,
    可以是客户端没有设置过listen的套接字,也可以是服务器端accept返回的套接字
  • buf:缓冲区,读到的信息存于此处
  • count:表示要读取的字节数
  • return:读到的字节数,对端退出返回0,读取失败返回-1

example

如下是server端代码,续接accept的example:

int main()
{
    // 1.创建套接字
    // 2.绑定
    // 3.设置监听状态
    // 4.获取连接
    // 5.读取对端发来的消息
    char inbuffer[BUFFER_SIZE];
    ssize_t s = read(serviceSock, inbuffer, sizeof(inbuffer) - 1);
    if (s > 0) // 读到的字节数
    {
        // read成功
        inbuffer[s] = '\0';
        std::cout << inbuffer << std::endl;
    }
}

write

向网络发送消息和写文件使用同一个API

函数原型及参数解析

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);
  • fd:网络套接字,用法对标read
  • buf:缓冲区,将缓冲区的内容发出
  • count:需要发送的字节数
  • 返回值:读到的字节数,对端退出返回0,发送失败返回-1

example

续接read的example:

int main()
{
    // 1.创建套接字
    // 2.绑定
    // 3.设置监听状态
    // 4.获取连接
    // 5.读取对端发来的消息
    char inbuffer[BUFFER_SIZE];
    ssize_t s = read(serviceSock, inbuffer, sizeof(inbuffer) - 1);
    if (s > 0) // 读到的字节数
    {
        // read成功
        inbuffer[s] = '\0';
        for (auto &e : inbuffer)
        {
            e = toupper(e);
        }
        write(listenSock_, inbuffer, s);
    }
}

将接收到的所有字符转为大写并发回

Tcp程序编写—字符转大写

如下是分别是客户端和服务器程序的源代码,服务器会将客户端发送过来的所有消息转为大写后发回,服务意义不大,旨在理解Tcp套接字的使用

工具包

util.h

#pragma once
#include "log.hpp"
#include <arpa/inet.h>
#include <cctype>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <netinet/in.h>
#include <signal.h>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define SOCKET_ERR 1
#define BIND_ERR 2
#define LISTEN_ERR 3
#define USAGE_ERR 4
#define CONN_ERR 5
#define BUFFER_SIZE 1024

服务器端

tcpServer.cc

#include "util.hpp"
class TcpServer
{
    struct ThreadData
    {
        int sock_;
        uint16_t clientPort_;
        std::string clientIp_;
        TcpServer *this_;
    };

private:
    // sock
    int listenSock_;
    // port
    uint16_t port_;
    // ip
    std::string ip_;

public:
    TcpServer(uint16_t port, std::string ip = "")
        : listenSock_(-1),
          port_(port),
          ip_(ip){}
    void init()
    {
        // 创建套接字
        listenSock_ = socket(AF_INET, SOCK_STREAM, 0);
        if (listenSock_ < 0)
        {
            logMssage(FATAL, "socket:%s", strerror(errno));
            exit(SOCKET_ERR);
        }
        logMssage(DEBUG, "socket:%s,%d", strerror(errno), listenSock_);

        // bind
        // 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)) == -1)
        {
            logMssage(FATAL, "bind:%s", strerror(errno));
            exit(BIND_ERR);
        }
        logMssage(DEBUG, "bind:%s,%d", strerror(errno), listenSock_);

        // 3.监听socket,因为tcp是面向连接的,所以要把自己设为可连接的状态
        if (listen(listenSock_, 5) < 0)
        {
            logMssage(FATAL, "listen:%s", strerror(errno));
            exit(LISTEN_ERR);
        }
        logMssage(DEBUG, "listen:%s,%d", strerror(errno), listenSock_);
    }
    void loop()
    {
        while (true)
        {
            // 4.获取连接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
            if (serviceSock < 0)
            {
                // 获取连接失败
                logMssage(WARINING, "accept:%s[%d]", strerror(errno), serviceSock);
                continue;
            }
            // 获取客户端的基本信息
            uint16_t peerPort = ntohs(peer.sin_port);
            std::string peerIp = inet_ntoa(peer.sin_addr);
            logMssage(DEBUG, "addept:%s | %s[%d]", strerror(errno), peerIp.c_str(), peerPort);
            // 5.提供服务,小写-->大写
#if 0
            // 5.1 v1版本,单进程版本,一旦进入,无法向后执行,同一时间只能为一个用户提供服务
            transService(serviceSock, peerIp, peerPort);
#elif 0
            // 5.2 v2.1版本,多进程,每个用户占据一个子进程
            signal(SIGCHLD, SIG_IGN);
            pid_t id = fork();
            assert(id != -1);
            if (id == 0)
            {
                // 子进车
                close(listenSock_);
                transService(serviceSock, peerIp, peerPort);
                exit(0); // 进入僵尸
            }
            // 父进程
            close(serviceSock); // 子进程关不了父进程的
                                // 可以非阻塞式等待,但比较复杂
                                // 可以signal忽略SIGCHILD信号
#elif 0
            // 5.2 v2.2版本,多进程,创造孤儿进程,无需忽略SIGCHILD
            pid_t id = fork();
            assert(id != -1);
            if (id == 0)
            {
                close(listenSock_);
                // 子进车
                if (fork() > 0)
                    exit(0); // 退出子进程
                // 孙子进程成为孤儿进程,由系统领养--回收问题由系统解决
                // 让孙子进程完成任务
                transService(serviceSock, peerIp, peerPort);
                exit(0); // 孙子进程退出
            }
            // 父进程
            close(serviceSock);                  // 孙子进程关不了父进程的
            pid_t ret = waitpid(id, nullptr, 0); // 回收子进程
            assert(ret > 0);
#else
            // 5.3 v3 多线程版本
            // 为线程提供的网络信息
            ThreadData *threadData = new ThreadData();
            threadData->clientIp_ = peerIp;
            threadData->clientPort_ = peerPort;
            threadData->sock_ = serviceSock;
            threadData->this_ = this;
            pthread_t tid;
            if (pthread_create(&tid, NULL, threadRoutine, threadData) < 0)
            {
                logMssage(WARINING, "pthread_create:%s", strerror(errno));
            }
#endif
            // debug
            //  logMssage(DEBUG, "server 开始提供服务...");
            //  sleep(1);
        }
    }
    static void *threadRoutine(void *args)
    {
        pthread_detach(pthread_self()); // 设置线程分离,无需主线程join
        ThreadData *td = static_cast<ThreadData *>(args);
        td->this_->transService(td->sock_, td->clientIp_, td->clientPort_);
        delete td;
    }
    // 将所有的的字母转为大写
    void transService(int sock, const std::string &clientIP, uint16_t clientPort)
    {
        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) // 读到的字节数
            {
                // read成功
                inbuffer[s] = '\0';
                if (strcasecmp(inbuffer, "quit") == 0)
                {
                    logMssage(DEBUG, "client quit -- %s[%d]", clientIP.c_str(), clientPort);
                    break;
                }
                // 进行大小写转化
                logMssage(DEBUG, "trans befor: %s", inbuffer);
                for (int i = 0; i < s; i++)
                {
                    if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
                    {
                        inbuffer[i] = toupper(inbuffer[i]);
                    }
                }
                logMssage(DEBUG, "trans after: %s", inbuffer);
                write(sock, inbuffer, strlen(inbuffer));
            }
            else if (s == 0)
            {
                // 代表对方关闭,client退出
                logMssage(DEBUG, "client quit -- %s[%d]", clientIP.c_str(), clientPort);
                break;
            }
            else
            {
                // 读取出错
                logMssage(WARINING, "%s[%d] -- read:%s", clientIP.c_str(), clientPort, strerror(errno));
                break;
            }
        }
        // client退出,服务到此结束
        close(sock); // 如果一个进程对应的文件fd,打开了没有归还,会造成文件描述符溢出
        logMssage(DEBUG, "server close %d done", sock);
    }
};

static void Usage(const std::string porc)
{
    std::cout << "Usage:\n\t" << porc << " port [IP]" << std::endl;
    std::cout << "example:\n\t" << porc << " 8080 127.0.0.1" << 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];
    }
    TcpServer srv(port, ip);
    srv.init();
    srv.loop();
    return 0;
}

客户端

tcpClient.cc

#include "util.hpp"
static void Usage(const std::string porc)
{
    std::cout << "Usage:\n\t" << porc << "IP port" << std::endl;
}
volatile static bool quit = false;
int main(int argc, char const *argv[])
{
    // 必须有传入IP 和 端口号
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    // 1.创建客户端
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    // ?2.不需要手动绑定,让系统自动为客户端分配IP和端口
    // ?3.不需要listen
    // ?4.不需要accept
    // 2.connect,向服务器发起连接请求
    // 从命令行参数获取服务器IP,端口
    std::string server_ip = argv[1];
    uint16_t server_port = atoi(argv[2]);
    // 2.1 先填充远端的主机信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;                    // 协议
    server.sin_port = htons(server_port);           // port
    inet_aton(server_ip.c_str(), &server.sin_addr); // ip
    // 2.2发起请求,connect会自动选择合适的IP和port进行bind
    if (connect(sockfd, (const struct sockaddr *)&server, sizeof(server)) != 0)
    {
        std::cerr << "connect: " << strerror(errno);
        exit(CONN_ERR);
    }
    std::cout << "info: connect success" << sockfd << std::endl;
    while (!quit)
    {
        std::string message;
        std::cout << "请输入:";
        std::getline(std::cin, message);
        if (strcasecmp(message.c_str(), "quit") == 0)
        {
            quit = true;
        }
        ssize_t s = write(sockfd, message.c_str(), message.size());
        if (s > 0)
        {
            message.resize(1024);
            ssize_t s = read(sockfd, (char *)(message.c_str()), 1024);
            std::cout << "Server Echo>>>" << message << std::endl;
        }
        else if (s <= 0)
        {
            break;
        }
    }
    close(sockfd);

    return 0;
}

TCP服务器(线程池版本)

上面的字符转换服务器我们分别尝试了单执行流、多进程、多线程的版本

单执行流同一时间只能对一个客户端进行服务,只有该客户端退出才能对下一个客户端进行服务

多线程和多进程的版本使用n个线程或进程同时对n个客户进行服务

多线程因为粒度更低,调用成本相对较低

但是,它们都是在完成网络连接之后,再为客户端现场新建一个线程/进程

我们不妨使用一个线程池,让服务器刚启动的时候就创建一些线程,一旦连接成功,直接可以交给线程池执行服务

为了提高趣味性,我们再改一下服务器提供的服务:使用popen()这个系统调用,让客户端可以向服务器发送一些命令让服务器执行,同时返回执行结果,如:客户端发送ls指令,服务器端便会发回它当前目录的文件

tcpServer.cc//tcp服务器源代码

#include "util.hpp"
class TcpServer
{
private:
    // sock
    int listenSock_;
    // port
    uint16_t port_;
    // ip
    std::string ip_;
    ThreadPool<Task> *tp_;

public:
    TcpServer(uint16_t port, std::string ip = "")
        : listenSock_(-1),
          port_(port),
          ip_(ip),
          tp_(nullptr)
    {}
    void init()
    {
        // 创建套接字
        listenSock_ = socket(AF_INET, SOCK_STREAM, 0);
        if (listenSock_ < 0)
        {
            logMssage(FATAL, "socket:%s", strerror(errno));
            exit(SOCKET_ERR);
        }
        logMssage(DEBUG, "socket:%s,%d", strerror(errno), listenSock_);

        // bind
        // 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)) == -1)
        {
            logMssage(FATAL, "bind:%s", strerror(errno));
            exit(BIND_ERR);
        }
        logMssage(DEBUG, "bind:%s,%d", strerror(errno), listenSock_);

        // 3.监听socket,因为tcp是面向连接的,所以要把自己设为可连接的状态
        if (listen(listenSock_, 5) < 0)
        {
            logMssage(FATAL, "listen:%s", strerror(errno));
            exit(LISTEN_ERR);
        }
        logMssage(DEBUG, "listen:%s,%d", strerror(errno), listenSock_);

        // 加载线程池
        tp_ = ThreadPool<Task>::getInstance();
    }
    void loop()
    {
        tp_->start(); // 启动线程池
        logMssage(DEBUG, "thread pool start success, thread num:%d", tp_->threadNum());
        while (true)
        {
            // 4.获取连接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
            if (serviceSock < 0)
            {
                // 获取连接失败
                logMssage(WARINING, "accept:%s[%d]", strerror(errno), serviceSock);
                continue;
            }
            // 获取客户端的基本信息
            uint16_t peerPort = ntohs(peer.sin_port);
            std::string peerIp = inet_ntoa(peer.sin_addr);
            logMssage(DEBUG, "addept:%s | %s[%d]", strerror(errno), peerIp.c_str(), peerPort);
            // 5.提供服务,线程池版本 
            Task t(serviceSock, peerIp, peerPort, std::bind(&TcpServer::execCommand, this, placeholders::_1, placeholders::_2, placeholders::_3));
            // bind: (this,sock,ip,port)-->(sock,ip,port)
            // C++11语法,详见包装器一文
            tp_->push(t); // 传入任务
        }
    }
    void execCommand(int sock, const std::string &clientIP, uint16_t clientPort)//调用popen完成对端发来的指令(循环接收,直到客户退出,断开连接)
    {
        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';
                logMssage(DEBUG, "[%s:%d] exec [%s]", clientIP.c_str(), clientPort, command);
                FILE *fp = popen(command, "r");
                if (fp == nullptr)
                {
                    logMssage(WARINING, "exec %s failed, beacuse:%s", command, strerror((errno)));
                    break;
                }
                // dup2(sock, fp->_fileno);//错误,注意区分文件读和写缓冲区
                // fflush(fp);
                char line[1024];
                while (fgets(line, sizeof(line), fp) != nullptr)
                {
                    write(sock, line, strlen(line));
                }
                pclose(fp);
                logMssage(DEBUG, "[%s:%d] exec [%s] ... done", clientIP.c_str(), clientPort, command);
            }
            else if (s == 0)
            {
                // 代表对方关闭,client退出
                logMssage(DEBUG, "client quit -- %s[%d]", clientIP.c_str(), clientPort);
                break;
            }
            else
            {
                // 读取出错
                logMssage(WARINING, "%s[%d] -- read:%s", clientIP.c_str(), clientPort, strerror(errno));
                break;
            }
        }
        // client退出,服务到此结束
        close(sock); // 如果一个进程对应的文件fd,打开了没有归还,会造成文件描述符溢出
        logMssage(DEBUG, "server close %d done", sock);
    }
};

static void Usage(const std::string porc)
{
    std::cout << "Usage:\n\t" << porc << " port [IP]" << std::endl;
    std::cout << "example:\n\t" << porc << " 8080 127.0.0.1" << 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];
    }
    TcpServer srv(port, ip);
    srv.init();
    srv.loop();
    return 0;
}

threadPool.hpp//具体线程池的编写可以看线程控制一文

#pragma once
#include "Lock.hpp"
#include <assert.h>
#include <iostream>
#include <pthread.h>
#include <queue>
#include <stdlib.h>
#include <sys/prctl.h> //更改线程名,便于调试查看
#include <unistd.h>
using namespace std;

const int gThreadNum = 5;
template <class T>
class ThreadPool
{
private:
    bool isStart;           // 判断防止当前线程池多次被启动
    int threadNum_;         // 线程的数量
    queue<T> taskQueue_;    // 任务队列
    pthread_mutex_t mutex_; // 保证访问任务队列是原子的
    pthread_cond_t cond_;   // 如果当前任务队列为空,让线程等待被唤醒
    bool quit_;
    static ThreadPool<T> *instance_; // 设计成单例模式

public:
    static ThreadPool<T> *getInstance()
    {
        static Mutex mutex;
        if (nullptr == instance_) // 仅仅过滤重复的判断
        {
            Lock_Guard lockGuard(&mutex); // 保护后面的内容
            if (nullptr == instance_)
            {
                instance_ = new ThreadPool<T>();
            }
        }

        return instance_;
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }

public:
    void start() // 创建多个线程,让它们等待被唤醒,执行push的任务
    {
        assert(isStart == false);
        isStart = true;
        for (int i = 0; i < threadNum_; i++)
        {
            pthread_t tmp;
            pthread_create(&tmp, nullptr, threadRoutine, this);
        }
    }
    void quit() // 关闭线程池时确保所有任务都完成了
    {
        while (haveTask())
        {
            pthread_cond_broadcast(&cond_);
            // usleep(1000);
            //  cout << taskQueue_.size() << endl;
        }
        quit_ = true;
    }
    void push(const T &in) // 在线程池中添加任务
    {
        lockQueue();
        taskQueue_.push(in);
        choiceThreadForHandl();
        unlockQueue();
    }
    int threadNum()
    {
        return threadNum_;
    }

private:
    ThreadPool(int threadNum = gThreadNum)
    {
        threadNum_ = threadNum;
        assert(threadNum > 0);
        isStart = false;
        quit_ = false;
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }
    ThreadPool(const ThreadPool<T> &) = delete;           // 单例防拷贝
    ThreadPool operator=(const ThreadPool<T> &) = delete; // 同上
    static void *threadRoutine(void *args)
    {
        prctl(PR_SET_NAME, "follower");
        pthread_detach(pthread_self());
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
        while (true) // 循环从任务队列中拿出任务并执行,队列为空则等待任务出现
        {
            tp->lockQueue();
            while (!tp->haveTask()) // 如果任务队列为空则等待
            {
                if (tp->quit_) // 当调用quit且队列已经为空的时候quit_才会被置为true
                {
                    cout << "quit" << endl;
                    return nullptr;
                }
                tp->waitForTask();
            }
            // 将任务从队列中拿到出来执行
            T t = tp->pop();
            tp->unlockQueue();

            t();
            // 规定所有任务内都有一个自己的run方法
        }
        return nullptr;
    }
    void lockQueue() // 加锁
    {
        pthread_mutex_lock(&mutex_);
    }
    void unlockQueue() // 解锁
    {
        pthread_mutex_unlock(&mutex_);
    }
    void waitForTask() // 让线程等待被唤醒
    {
        pthread_cond_wait(&cond_, &mutex_);
    }
    bool haveTask() // 队列不为空
    {
        return !taskQueue_.empty();
    }
    void choiceThreadForHandl() // 随便唤醒一个等待的线程
    {
        pthread_cond_signal(&cond_);
    }
    T pop() // 从队列中拿取一个任务
    {
        T tmp = taskQueue_.front();
        taskQueue_.pop();
        return tmp;
    }
};
template <class T>
ThreadPool<T> *ThreadPool<T>::instance_ = nullptr; // 单例

Task.hpp//提供任务类,可使用回调的方法给线程池传入任务

#pragma once
#include "log.hpp"
#include <functional>
#include <iostream>
#include <string>
class Task
{
    using callback_t = std::function<void(int, std::string, uint16_t)>;
    // 等价于std::function<void(int, std::string, uint16_t)> callback_t;
private:
    int sock_;
    std::string ip_;
    uint16_t port_;
    callback_t func_;
public:
    Task() : sock_(-1), port_(-1) {}
    Task(int sock, std::string ip, uint16_t port, callback_t func)
        : sock_(sock), ip_(ip), port_(port), func_(func)
    {}
    void operator()()
    {
        logMssage(DEBUG, "线程ID[%p]处理%s:%d的请求开始了...", pthread_self(), ip_.c_str(), port_);
        func_(sock_, ip_, port_);
        logMssage(DEBUG, "线程ID[%p]处理%s:%d的请求完成了...", pthread_self(), ip_.c_str(), port_);
    }
};

Lock.hpp//封装了互斥锁、设计了RAII的LockGard,如果熟悉C++线程库,可以直接使用C++线程库

#pragma once
#include <iostream>
#include <pthread.h>

class Mutex
{
private:
    pthread_mutex_t lock_;

public:
    Mutex()
    {
        pthread_mutex_init(&lock_, nullptr);
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&lock_);
    }
    void lock()
    {
        pthread_mutex_lock(&lock_);
    }
    void unlock()
    {
        pthread_mutex_unlock(&lock_);
    }
};

class Lock_Guard
{
private:
    Mutex *mutex_;

public:
    Lock_Guard(Mutex *mutex)
        : mutex_(mutex)
    {
        mutex_->lock();
    }
    ~Lock_Guard()
    {
        mutex_->unlock();
    }
};

log.hpp//提供日志函数,方便打印详细的日志信息

#pragma once

#include <stdlib.h>

#include <cassert>
#include <cstdarg>
#include <cstdio>
#include <ctime>
//日志等级
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3

const char* log_leval[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};
void logMssage(int level, const char* format, ...)
{
    assert(level >= DEBUG);
    assert(level <= FATAL);
    const char* name = getenv("USER");
    char logInfo[1024];
    va_list ap;
    va_start(ap, format);  //让dp对应到可变部分(...)
    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_leval[level],
            (unsigned)time(NULL), name == NULL ? "nukonw" : name,
            logInfo);
}

util.hpp//工具包:包含了所有要包含的头文件和一些宏定义

#pragma once
#include "Lock.hpp"
#include "Task.hpp"
#include "ThreadPool.hpp"
#include "log.hpp"
#include <arpa/inet.h>
#include <cctype>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <netinet/in.h>
#include <signal.h>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define SOCKET_ERR 1
#define BIND_ERR 2
#define LISTEN_ERR 3
#define USAGE_ERR 4
#define CONN_ERR 5
#define BUFFER_SIZE 1024

将服务器端“守护进程”化

一般服务器进程都是以守护进程的形式出现的(具体守护进程的概念,见《[进程概念](#(572条消息) 进程概念(Linux)_Massachusetts_11的博客-CSDN博客)》->守护进程),一旦启动之后,除非用户主动关闭,否则一直会在运行

setid()可以更改当前进程的会话ID

但是调用此函数的进程不能是一个进程的组长

所以,一般我们需要fork()一个子进程,让子进程setsid,父进程可以直接exit();

if(fork() > 0) exit(0);
setsid(1);

除了守护进程化,一般服务器程序还要进行一些选做内容

  • 忽略SIGPIPE信号

    如果server端在write时,Client已经退出,则server端也会被SIGPIPE信号终止,所以我们要忽略此信号

  • 更改进程的工作目录:

    chdir();//《进程控制》一文可以看到

  • 删除/修改0,1,2号文件描述符

    因为一般服务器端不会在标准输入输出流进行输入输出

    所以我们可以将0,1,2号文件描述符关掉,但是很少有人这么做

    在Linux下有一个“垃圾桶”或者说“文件黑洞”,

    凡是写入/dev/null中的数据,一概会被丢弃,从中读取也是空

    所以,我们可以打开 /dev/null,并且对0,1,2进行重定向

    或者也可以创建一个日志文件,将产生的日志信息存储到文件中去

daemaonize.hpp

#pragma once

#include <cstdio>
#include <fcntl.h>
#include <iostream>
#include <signal.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
void daemonize() // 将程序守护进程化
{
    // 1. 忽略SIGPIPE
    signal(SIGPIPE, SIG_IGN);
    // 2. 更改进程的工作目录
    // chdir();
    // 3. 让自己不要成为进程组组长
    if (fork() > 0)
        exit(0);
    // 4.设置自己时一个独立的会话
    setsid();
    // 5. 重定向0,1,2
    int fd = 0;
    if ((fd = open("/dev/null", O_RDWR) != -1))
    {
        dup2(fd, STDIN_FILENO);
        dup2(fd, STDOUT_FILENO);
        dup2(fd, STDERR_FILENO);
        // 关闭掉不需要的fd
        if (fd > STDERR_FILENO)
            close(fd);
    }
}

log.hpp

#pragma once

#include <cassert>
#include <cstdarg>
#include <cstdio>
#include <ctime>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
// 日志等级
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3
#define LOGFILE "tcpServer.log"

const char* log_leval[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};
void logMssage(int level, const char* format, ...)
{
    assert(level >= DEBUG);
    assert(level <= FATAL);
    const char* name = getenv("USER");
    char logInfo[1024];
    va_list ap;
    va_start(ap, format);  //让dp对应到可变部分(...)
    vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);
    va_end(ap);  // ap = NULL

    int fd = open(LOGFILE, O_WRONLY | O_CREAT | O_APPEND, 0666);
    assert(fd > 0);

    FILE *out = (level == FATAL) ? stderr : stdout;

    dup2(fd, 1);
    dup2(fd, 2);
    fprintf(out, "%s | %u | %s | %s\n", log_leval[level],
            (unsigned)time(NULL), name == NULL ? "nukonw" : name,
            logInfo);
    fflush(out); // 将C缓冲区的数据刷新到OS
    fsync(fd);   // 将OS中的数据尽快刷盘
}

只需要在服务器端的main函数调用daemonize()即可完成守护进程化

tcpServer.cc

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];
    }

    daemonize(); // 我们的进程将成为守护进程

    TcpServer srv(port, ip);
    srv.init();
    srv.loop();
    return 0;
}

一般守护进程化的程序结尾带一个d

makefile

.PHONY:all
all:tcpClient tcpServerd

tcpClient:tcpClient.cc
	g++ -o $@ $^ -std=c++11 -lpthread
tcpServerd:tcpServer.cc
	g++ -o $@ $^ -std=c++11 -lpthread

.PHONY:clean
clean:
	rm -f tcpClient tcpServerd

此时,我们的Tcp服务器就成为了守护进程

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值