Linux网络编程—Socket编程

第一章:Socket编程UDP

补充:

V1版本 - Echo server

简单的回显服务器和客户端代码

log.hpp

#pragma once

#include <iostream>
#include <stdarg.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>


using namespace std;

#define SIZE 1024

//表示不同严重程度
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1 //打印到屏幕
#define Onefile 2 //所有日志写入同一个文件
#define Classfile 3 //按日志级别分文件

#define LogFile "log.txt"

class Log {
public:
    //默认输出到屏幕,日志目录为 ./log/
    Log() { 
        printMethod = Screen; 
        path = "./log/";
    }
    void Enable(int method) { printMethod = method; } //设置日志输出方式

    string levelToString(int level) { //把数字型的日志级别 → 转成人类可读的字符串
        switch (level) {
        case Info: return "Info";
        case Debug: return "Debug";
        case Warning: return "Warning";
        case Error: return "Error";
        case Fatal: return "Fatal";
        default: return "None";    
        }
    }

    void printLog(int level, const string& logtxt) { //根据 printMethod 的值决定日志去哪
        switch (printMethod) {
        case Screen:
            cout << logtxt << endl;
            break;
        case Onefile:
            printOneFile(LogFile, logtxt);
            break;
        case Classfile:
            printClassFile(level, logtxt);
            break;
        default:
            break;
        }
    }

    void printOneFile(const string& logname, const string& logtxt) {
        string _logname = path + logname;
        int fd = open(_logname.c_str(), O_WRONLY|O_CREAT|O_APPEND, 0666);
        if (fd < 0 ) return;
        write(fd, logtxt.c_str(), logtxt.size());
        close(fd);
    }

    void printClassFile(int level, const string& logtxt) {
        string filename = LogFile;
        filename += ".";
        filename += levelToString(level);
        printOneFile(filename, logtxt);
    }

    void operator()(int level, const char* format, ...) {
        //获取当前时间 → 格式化到 leftbuffer
        time_t t = time(nullptr);
        struct tm* ctime = localtime(&t);
        
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
        ctime->tm_year+1900, ctime->tm_mon+1, ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        //处理用户传入的可变参数 → 格式化到 rightbuffer
        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        //拼接成完整日志字符串
        char logtxt[SIZE*2];
        snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
        //格式:默认部分+自定义部分

        // printf("%s", logtxt);
        printLog(level, logtxt);    
    }

    ~Log() {}
private:
    int printMethod;
    string path;
};

UdpServer.hpp

#pragma once

#include <iostream>
#include <sys/types.h> 
#include <sys/socket.h>
#include <string>
#include <cstring>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <unordered_map>
#include "log.hpp"

Log lg;

enum {
    SOCEKT_ERR = 1,
    BIND_ERR
};

uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;

class UdpServer {
public:
    UdpServer(const uint16_t& port = defaultport, const std::string& ip = defaultip) 
        :sockfd_(0), port_(port), ip_(ip), isrunning_(false) {}
    
    void Init() {
        //1. 创建udp socket
        //AF_INET - IPv4, SOCK_DGRAM - 表示使用数据报套接字,即UDP协议。
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd_ < 0) {
            lg(Fatal, "socket create error, sockfd:%d", sockfd_);
            exit(SOCEKT_ERR);
        }
        lg(Info, "create socket success, sockfd:%d", sockfd_);

        //2.bind socket 绑定端口号
        //int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 
        //const struct sockaddr *addr只是设计接口用,实际不使用
        struct sockaddr_in local;
        bzero(&local, sizeof(local));//清零
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);//要保证端口号是网络字节序列,因为要发给对方。htons主机序列转网络序列
        //sin_addr是结构体,sin_addr才是成员。inet_addr参数是const char* 
        local.sin_addr.s_addr = inet_addr(ip_.c_str());//1.string -> uint32_t  2.uint32_t必须是网络序列
        // local.sin_addr.s_addr = htonl(INADDR_ANY);//任意地址绑定。INADDR_ANY是全0,不存在网络字节序转换,htonl可以省略
        
        if (bind(sockfd_, (const struct sockaddr*)&local, sizeof(local)) < 0) {
            lg(Fatal, "bind error, errno:%d, err string:%s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        lg(Info, "bind success, errno:%d, err string:%s", errno, strerror(errno));
    }

    void Run() {
        isrunning_ = true;
        char inbuffer[size];
        cout << "server start running, waiting for message..." << endl;
        while (isrunning_) {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer)-1, 0, (struct sockaddr*)&client, &len);
            if (n < 0) {
                lg(Warning, "recvfrom error, errno:%d, err string:%s", errno, strerror(errno));
                continue;
            }
            //充当一次数据处理
            inbuffer[n] = 0;
            std::string info = inbuffer;
            std::string echo_string = "server echo# " + info;
            cout << echo_string << endl;

            sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&client, len);
        }
    }

    ~UdpServer() { if (sockfd_ > 0) close(sockfd_); }
private:
    int sockfd_;//网络文件描述符
    std::string ip_;//任意地址bind 0
    uint16_t port_;//端口号
    bool isrunning_;
}; 

UdpClient.cc

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "Terminal.hpp"

using namespace std;

void Usage(std::string proc) {
    std::cout << "\n\tUsage: " << proc << " serverip serverport\n" << std::endl;
}

// ./udpclient serverip serverport
int main(int argc, char* argv[]) {
    if (argc != 3) {
        Usage(argv[0]);
        exit(0);
    }
    string serverip = argv[1];
    uint16_t serverport = stoi(argv[2]);

    //构建服务器套接字
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);//要保证端口号是网络字节序列,因为要发给对方。htons主机序列转网络序列
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    socklen_t len = sizeof(server);

    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        cout << "socker error" << endl;
        return 1;
    }
    // 客户端一定要bind,因为需要自己的IP和端口。只不过不需要用户显示的bind,一般由OS自由随机选择
    // 一个端口号只能被一个进程bind,sever和client都是如此
    // 如果允许客户端显示绑定端口号,那么各服务器厂商还得协调,否则会冲突
    // client的port是多少不重要,只要能保证主机上的唯一性就可以
    //系统什么时候给客户端bind?首次发送数据的时候
    string message;
    char buffer[1024];
    while (true) {
        //如果是群聊,那么只要不输入消息就会在getline阻塞导致收不到消息
        cout << "Please Enter@ ";
        getline(cin, message);
        sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);

        struct sockaddr_in tmp;
        socklen_t len = sizeof(tmp);
        ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&tmp, &len);
        if (s > 0) {
            buffer[s] = 0;
            cout << buffer << endl;
        }
    }

    close(sockfd);
    return 0;
}

Main.cc

#include "UdpServer.hpp"
#include <memory>
#include <cstdio>
#include <vector>

// "159.75.96.122" 点分十进制字符串风格的IP地址。用户习惯这种类型的IP地址
void Usage(std::string proc) {
    std::cout << "\n\tUsage: " << proc << " prot[1024+]\n" << std::endl;
}

// ./udpserver port 命令行启动时传入端口号
int main(int argc, char* argv[]) {
    if (argc != 2) {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port));
    svr->Init();
    svr->Run();
    return 0;
}

V2 版本 - 接收数据和处理数据解耦

可以接收Linux指令

log.hpp 同上

UdpServer.hpp

//接收数据和处理数据解耦版本
// using func_t = std::function<std::string(const std::string&)>;
typedef std::function<std::string(const std::string&)> func_t;

Log lg;
// extern Log lg; //extern声明(告诉编译器x在别的文件定义了)

enum {
    SOCEKT_ERR = 1,
    BIND_ERR
};

uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;

class UdpServer {
public:
    UdpServer(const uint16_t& port = defaultport, const std::string& ip = defaultip) 
        :sockfd_(0), port_(port), ip_(ip), isrunning_(false) {}
    
    void Init() {
        //1. 创建udp socket
        //AF_INET - IPv4, SOCK_DGRAM - 表示使用数据报套接字,即UDP协议。
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd_ < 0) {
            lg(Fatal, "socket create error, sockfd:%d", sockfd_);
            exit(SOCEKT_ERR);
        }
        lg(Info, "create socket success, sockfd:%d", sockfd_);

        //2.bind socket 绑定端口号
        //int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 
        //const struct sockaddr *addr只是设计接口用,实际不使用
        struct sockaddr_in local;
        bzero(&local, sizeof(local));//清零
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);//要保证端口号是网络字节序列,因为要发给对方。htons主机序列转网络序列
        //sin_addr是结构体,sin_addr才是成员。inet_addr参数是const char* 
        local.sin_addr.s_addr = inet_addr(ip_.c_str());//1.string -> uint32_t  2.uint32_t必须是网络序列
        // local.sin_addr.s_addr = htonl(INADDR_ANY);//任意地址绑定。INADDR_ANY是全0,不存在网络字节序转换,htonl可以省略
        
        if (bind(sockfd_, (const struct sockaddr*)&local, sizeof(local)) < 0) {
            lg(Fatal, "bind error, errno:%d, err string:%s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        lg(Info, "bind success, errno:%d, err string:%s", errno, strerror(errno));
    }

    void Run(func_t func) { //对代码进行分层
        isrunning_ = true;
        char inbuffer[size];
        cout << "server start running, waiting for message..." << endl;
        while (isrunning_) {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer)-1, 0, (struct sockaddr*)&client, &len);
            if (n < 0) {
                lg(Warning, "recvfrom error, errno:%d, err string:%s", errno, strerror(errno));
                continue;
            }
            //充当一次数据处理
            inbuffer[n] = 0;
            std::string info = inbuffer;
            std::string echo_string = func(info);
            sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&client, len);
        }
    }

    //可以不关,析构对象时服务器也关了,进程也结束了。文件生命周期是随进程的
    ~UdpServer() { if (sockfd_ > 0) close(sockfd_); }
private:
    int sockfd_;//网络文件描述符
    std::string ip_;//任意地址bind 0
    uint16_t port_;//端口号
    bool isrunning_;
};

UdpClient.cc 同上

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "Terminal.hpp"

using namespace std;

void Usage(std::string proc) {
    std::cout << "\n\tUsage: " << proc << " serverip serverport\n" << std::endl;
}

// ./udpclient serverip serverport
int main(int argc, char* argv[]) {
    if (argc != 3) {
        Usage(argv[0]);
        exit(0);
    }
    string serverip = argv[1];
    uint16_t serverport = stoi(argv[2]);

    //构建服务器套接字
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);//要保证端口号是网络字节序列,因为要发给对方。htons主机序列转网络序列
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    socklen_t len = sizeof(server);

    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        cout << "socker error" << endl;
        return 1;
    }
    // 客户端一定要bind,因为需要自己的IP和端口。只不过不需要用户显示的bind,一般由OS自由随机选择
    // 一个端口号只能被一个进程bind,sever和client都是如此
    // 如果允许客户端显示绑定端口号,那么各服务器厂商还得协调,否则会冲突
    // client的port是多少不重要,只要能保证主机上的唯一性就可以
    //系统什么时候给客户端bind?首次发送数据的时候
    string message;
    char buffer[1024];
    while (true) {
        //如果是群聊,那么只要不输入消息就会在getline阻塞导致收不到消息
        cout << "Please Enter@ ";
        getline(cin, message);
        sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);

        struct sockaddr_in tmp;
        socklen_t len = sizeof(tmp);
        ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&tmp, &len);
        if (s > 0) {
            buffer[s] = 0;
            cout << buffer << endl;
        }
    }

    close(sockfd);
    return 0;
}

Main.cc

#include "UdpServer.hpp"
#include <memory>
#include <cstdio>
#include <vector>

// "159.75.96.122" 点分十进制字符串风格的IP地址。用户习惯这种类型的IP地址
void Usage(std::string proc) {
    std::cout << "\n\tUsage: " << proc << " prot[1024+]\n" << std::endl;
}

bool SafeCheck(const std::string& cmd) {
    int safe = false;
    std::vector<std::string> key_word = {
        "rm", "mv", "cp", "kill", "sudo", "unlink", "uninstall", "yum", "top", "while"
    };
    for (auto& word : key_word) {
        auto pos = cmd.find(word);
        if (pos != std::string::npos) return false;
    }
    return true;
}

//假设发过来的是指令。先解析字符串指令,ls -a -l -> "ls" "-a" "-l"  在调用exec*()系列函数
std::string ExcuteCommand(const std::string& cmd) {
    std::cout << "get a request cmd:" << cmd << std::endl;
    //直接使用popen
    if (SafeCheck(cmd) == false) return "Bad man";
    FILE* fp = popen(cmd.c_str(), "r");
    if (nullptr == fp) {
        perror("popen");
        return "error";
    }
    std::string result;
    char buffer[4096];
    while (true) {
        char* ok = fgets(buffer, sizeof(buffer), fp);
        if (ok == nullptr) break;
        result += buffer;
    }
    pclose(fp);
    return result;
}

std::string Handler(const std::string& info) {
	std::string res = "Server get a message: ";
	res += info;
	std::cout << res << std::endl;
	return res;
}

// ./udpserver port 命令行启动时传入端口号
int main(int argc, char* argv[]) {
    if (argc != 2) {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port));
    svr->Init();
    svr->Run(Handler);
    //svr->Run(ExcuteCommand); //处理指令的版本
    return 0;
}

V3 版本 - 验证UDP - windows 作为client访问

log.hpp 同上

UdpServer.hpp 同上

// using func_t = std::function<std::string(const std::string&)>;
typedef std::function<std::string(const std::string&)> func_t;

Log lg;
// extern Log lg; //extern声明(告诉编译器x在别的文件定义了)

enum {
    SOCEKT_ERR = 1,
    BIND_ERR
};

uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;

class UdpServer {
public:
    UdpServer(const uint16_t& port = defaultport, const std::string& ip = defaultip) 
        :sockfd_(0), port_(port), ip_(ip), isrunning_(false) {}
    
    void Init() {
        //1. 创建udp socket
        //AF_INET - IPv4, SOCK_DGRAM - 表示使用数据报套接字,即UDP协议。
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd_ < 0) {
            lg(Fatal, "socket create error, sockfd:%d", sockfd_);
            exit(SOCEKT_ERR);
        }
        lg(Info, "create socket success, sockfd:%d", sockfd_);

        //2.bind socket 绑定端口号
        //int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 
        //const struct sockaddr *addr只是设计接口用,实际不使用
        struct sockaddr_in local;
        bzero(&local, sizeof(local));//清零
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);//要保证端口号是网络字节序列,因为要发给对方。htons主机序列转网络序列
        //sin_addr是结构体,sin_addr才是成员。inet_addr参数是const char* 
        local.sin_addr.s_addr = inet_addr(ip_.c_str());//1.string -> uint32_t  2.uint32_t必须是网络序列
        // local.sin_addr.s_addr = htonl(INADDR_ANY);//任意地址绑定。INADDR_ANY是全0,不存在网络字节序转换,htonl可以省略
        
        if (bind(sockfd_, (const struct sockaddr*)&local, sizeof(local)) < 0) {
            lg(Fatal, "bind error, errno:%d, err string:%s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        lg(Info, "bind success, errno:%d, err string:%s", errno, strerror(errno));
    }

    void Run(func_t func) { //对代码进行分层
        isrunning_ = true;
        char inbuffer[size];
        cout << "server start running, waiting for message..." << endl;
        while (isrunning_) {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer)-1, 0, (struct sockaddr*)&client, &len);
            if (n < 0) {
                lg(Warning, "recvfrom error, errno:%d, err string:%s", errno, strerror(errno));
                continue;
            }
            //充当一次数据处理
            inbuffer[n] = 0;
            std::string info = inbuffer;
            std::string echo_string = func(info);
            sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&client, len);
        }
    }

    //可以不关,析构对象时服务器也关了,进程也结束了。文件生命周期是随进程的
    ~UdpServer() { if (sockfd_ > 0) close(sockfd_); }
private:
    int sockfd_;//网络文件描述符
    std::string ip_;//任意地址bind 0
    uint16_t port_;//端口号
    bool isrunning_;
};

udp_client.cpp

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <tchar.h>
#include <cstdio>
#include <string>
#include <WinSock2.h>
#include <Windows.h>
#include <WS2tcpip.h>
using namespace std;
//#pragma warning(disable:4996)
#pragma comment(lib, "ws2_32.lib")

uint16_t serverport = 8888;
string serverip = "159.75.96.122";

int main() {
	cout << "hello world" << endl;
    WSAData wsd;
	WSAStartup(MAKEWORD(2, 2), &wsd);
	
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);//要保证端口号是网络字节序列,因为要发给对方。htons主机序列转网络序列
    //server.sin_addr.s_addr = inet_addr(serverip.c_str());
    inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);

    SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        cout << "socker error" << endl;
        return 1;
    }

    string message;
    char buffer[1024];
    while (true) {
        cout << "Please Enter@ ";
        getline(cin, message);
        // cout << message << endl;
        sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));

        struct sockaddr_in tmp;
        int len = sizeof(tmp);
        int s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&tmp, &len);
        if (s > 0) {
            buffer[s] = 0;
            cout << buffer << endl;
        }
    }

    closesocket(sockfd);
	WSACleanup();
	return 0;
}

Main.cc 同上

#include "UdpServer.hpp"
#include <memory>
#include <cstdio>
#include <vector>

// "159.75.96.122" 点分十进制字符串风格的IP地址。用户习惯这种类型的IP地址
void Usage(std::string proc) {
    std::cout << "\n\tUsage: " << proc << " prot[1024+]\n" << std::endl;
}

bool SafeCheck(const std::string& cmd) {
    int safe = false;
    std::vector<std::string> key_word = {
        "rm", "mv", "cp", "kill", "sudo", "unlink", "uninstall", "yum", "top", "while"
    };
    for (auto& word : key_word) {
        auto pos = cmd.find(word);
        if (pos != std::string::npos) return false;
    }
    return true;
}

//假设发过来的是指令。先解析字符串指令,ls -a -l -> "ls" "-a" "-l"  在调用exec*()系列函数
std::string ExcuteCommand(const std::string& cmd) {
    std::cout << "get a request cmd:" << cmd << std::endl;
    //直接使用popen
    if (SafeCheck(cmd) == false) return "Bad man";
    FILE* fp = popen(cmd.c_str(), "r");
    if (nullptr == fp) {
        perror("popen");
        return "error";
    }
    std::string result;
    char buffer[4096];
    while (true) {
        char* ok = fgets(buffer, sizeof(buffer), fp);
        if (ok == nullptr) break;
        result += buffer;
    }
    pclose(fp);
    return result;
}

std::string Handler(const std::string& info) {
	std::string res = "Server get a message: ";
	res += info;
	std::cout << res << std::endl;
	return res;
}

// ./udpserver port 命令行启动时传入端口号
int main(int argc, char* argv[]) {
    if (argc != 2) {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port));
    svr->Init();
    svr->Run(Handler);
    //svr->Run(ExcuteCommand); //处理指令的版本
    return 0;
}

  1. WinSock2.h是Windows Sockets API(应用程序接口)的头文件,用于在Windows平台上进行网络编程。它包含了Windows Sockets 2(Winsock2)所需的数据类型、函数声明和结构定义,使得开发者能够创建和使用套接字(sockets)进行网络通信。
  2. 在编写使用Winsock2的程序时,需要在源文件中包含WinSock2.h头文件。这样,编译器就能够识别并理解Winsock2中定义的数据类型和函数,从而能够正确地编译和链接网络相关的代码。
  3. 此外,与WinSock2.h头文件相对应的是ws2_32.lib库文件。在链接阶段,需要将这个库文件链接到程序中,以确保运行时能够找到并调用Winsock2 API中实现的函数。
  4. 在WinSock2.h中定义了一些重要的数据类型和函数,如:
  5. WSADATA:保存初始化Winsock库时返回的信息。
  6. SOCKET:表示一个套接字描述符,用于在网络中唯一标识一个套接字。
  7. sockaddr_in:IPv4地址结构体,用于存储IP地址和端口号等信息。
  8. socket():创建一个新的套接字。
  9. bind():将套接字与本地地址绑定。
  10. listen():将套接字设置为监听模式,等待客户端的连接请求。
  11. accept():接受客户端的连接请求,并返回一个新的套接字描述符,用于与客户端进行通信。

  1. WSAStartup函数是Windows Sockets API的初始化函数,它用于初始化Winsock库。该函数在应用程序或DLL调用任何Windows套接字函数之前必须首先执行,它扮演着初始化的角色。
  2. 以下是WSAStartup函数的一些关键点:
  3. 它接受两个参数:wVersionRequested和lpWSAData。wVersionRequested用于指定所请求的Winsock版本,通常使用MAKEWORD(major, minor)宏,其中major和minor分别表示请求的主版本号和次版本号。lpWSAData是一个指向WSADATA结构的指针,用于接收初始化信息。
  4. 如果函数调用成功,它会返回0;否则,返回错误代码。
  5. WSAStartup函数的主要作用是向操作系统说明我们将使用哪个版本的Winsock库,从而使得该库文件能与当前的操作系统协同工作。成功调用该函数后,Winsock库的状态会被初始化,应用程序就可以使用Winsock提供的一系列套接字服务,如地址家族识别、地址转换、名字查询和连接控制等。这些服务使得应用程序可以与底层的网络协议栈进行交互,实现网络通信。
  6. 在调用WSAStartup函数后,如果应用程序完成了对请求的Socket库的使用,应调用WSACleanup函数来解除与Socket库的绑定并释放所占用的系统资源。

V4版本 - 简单聊天室

log.hpp 同上

UdpServer.hpp

#pragma once

#include <iostream>
#include <sys/types.h> 
#include <sys/socket.h>
#include <string>
#include <cstring>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <unordered_map>
#include "log.hpp"

// using func_t = std::function<std::string(const std::string&)>;
typedef std::function<std::string(const std::string&, const std::string&, uint16_t)> func_t;

Log lg;
// extern Log lg; //extern声明(告诉编译器x在别的文件定义了)

enum {
    SOCEKT_ERR = 1,
    BIND_ERR
};

uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;

class UdpServer {
public:
    UdpServer(const uint16_t& port = defaultport, const std::string& ip = defaultip) 
        :sockfd_(0), port_(port), ip_(ip), isrunning_(false) {}
    
    void Init() {
        //1. 创建udp socket
        //2. UDP的socket是全双工的,允许被同时读写
        //AF_INET - IPv4, SOCK_DGRAM - 表示使用数据报套接字,即UDP协议。
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd_ < 0) {
            lg(Fatal, "socket create error, sockfd:%d", sockfd_);
            exit(SOCEKT_ERR);
        }
        lg(Info, "create socket success, sockfd:%d", sockfd_);

        //2.bind socket 绑定端口号
        //int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 
        //const struct sockaddr *addr只是设计接口用,实际不使用
        struct sockaddr_in local;
        bzero(&local, sizeof(local));//清零
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);//要保证端口号是网络字节序列,因为要发给对方。htons主机序列转网络序列
        //sin_addr是结构体,sin_addr才是成员。inet_addr参数是const char* 
        local.sin_addr.s_addr = inet_addr(ip_.c_str());//1.string -> uint32_t  2.uint32_t必须是网络序列
        // local.sin_addr.s_addr = htonl(INADDR_ANY);//任意地址绑定。INADDR_ANY是全0,不存在网络字节序转换,htonl可以省略
        
        if (bind(sockfd_, (const struct sockaddr*)&local, sizeof(local)) < 0) {
            lg(Fatal, "bind error, errno:%d, err string:%s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        lg(Info, "bind success, errno:%d, err string:%s", errno, strerror(errno));
    }

    void CheckUser(const struct sockaddr_in& client, const std::string& clientip, const uint16_t& clientport) {
        auto iter = onlie_user_.find(clientip);
        if (iter == onlie_user_.end()) {
            onlie_user_.insert({clientip, client});
            std::cout << "[" << clientip << ":" << clientport << "]# add to online user list." << std::endl;
        }
    }

    void Broadcast(const std::string& info, const std::string& clientip, const uint16_t& clientport) {
        for (const auto& user : onlie_user_) {
            std::string message = "[";
            message += clientip;
            message += ":";
            message += clientport;
            message += "]# ";
            message += info;
            socklen_t len = sizeof(user.second);
            sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr*)&user.second, len);
            
        }
    }

    void Run() { //对代码进行分层
        isrunning_ = true;
        char inbuffer[size];
        cout << "server start running, waiting for message..." << endl;
        while (isrunning_) {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer)-1, 0, (struct sockaddr*)&client, &len);
            if (n < 0) {
                lg(Warning, "recvfrom error, errno:%d, err string:%s", errno, strerror(errno));
                continue;
            }
            //检查收到该次信息的发送用户是否为新用户
            uint16_t clientport = ntohs(client.sin_port);//网络序列转主机序列
            std::string clientip = inet_ntoa(client.sin_addr);//整数IP转字符串IP
            CheckUser(client, clientip, clientport);

            //将消息广播给所有在线用户
            std::string info = inbuffer;
            Broadcast(info, clientip, clientport);
        }
    }

    //可以不关,析构对象时服务器也关了,进程也结束了。文件生命周期是随进程的
    ~UdpServer() { if (sockfd_ > 0) close(sockfd_); }
private:
    int sockfd_;//网络文件描述符
    std::string ip_;//任意地址bind 0
    uint16_t port_;//端口号
    bool isrunning_;
    std::unordered_map<std::string, struct sockaddr_in> onlie_user_;
    //维护在线用户列表  主机版本IP地址    网络版本套接字信息
};

Terminal.hpp

//为了解决输入消息和接收消息都混到同一个会话
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

std::string terminal = "/dev/pts/1";

int OpenTerminal() {
    int fd = open(terminal.c_str(), O_WRONLY);
    if (fd < 0) {
        std::cerr << "open terminal error" << std::endl;
        return 1;
    }
    // std::cout << "fd: " << fd << std::endl;
    dup2(fd, 2);
    // printf("hello world\n");
    return 0;
}

UdpClient.cc

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "Terminal.hpp"

using namespace std;

//多线程,群聊版本(可以一边看消息,一边输出消息)
void Usage(std::string proc) {
    std::cout << "\n\tUsage: " << proc << " serverip serverport\n" << std::endl;
}

struct ThreadData {
    struct sockaddr_in server;
    int sockfd;
    std::string serverip;//用于获取谁上线
};

void* recv_message(void* args) {
    // OpenTerminal(); // ./udpclient 159.75.96.122 8888 2>/dev/pts/1 该方式也是同样效果
    ThreadData* td = static_cast<ThreadData*>(args);
    char buffer[1024];
    while (true) {
        memset(buffer, 0, sizeof(buffer));
        struct sockaddr_in tmp;
        socklen_t len = sizeof(tmp);
        ssize_t s = recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr*)&tmp, &len);
        if (s > 0) {
            buffer[s] = 0;
            cerr << buffer << endl;
        }
    }
}

void* send_message(void* args) {
    ThreadData* td = static_cast<ThreadData*>(args);
    string message;
    socklen_t len = sizeof(td->server);

    std::string welcome = td->serverip;
    welcome += "comming...";
    //没有登录功能,通过发消息方式获取谁上线(即一上线就能获取,而不是第一次发消息)
    sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&(td->server), len);

    while (true) {
        cout << "Please Enter@ ";
        getline(cin, message);
        sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&(td->server), len);
    }
}

// ./udpclient serverip serverport
int main(int argc, char* argv[]) {
    if (argc != 3) {
        Usage(argv[0]);
        exit(0);
    }
    string serverip = argv[1];
    uint16_t serverport = stoi(argv[2]);

    //构建服务器套接字
    struct ThreadData td;
    bzero(&td.server, sizeof(td.server));
    td.server.sin_family = AF_INET;
    td.server.sin_port = htons(serverport);//要保证端口号是网络字节序列,因为要发给对方。htons主机序列转网络序列
    td.server.sin_addr.s_addr = inet_addr(serverip.c_str());
    td.serverip = serverip;//用于获取谁上线
    
    td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (td.sockfd < 0) {
        cout << "socker error" << endl;
        return 1;
    }
      
    pthread_t recvr, sender;
    pthread_create(&recvr, nullptr, recv_message, &td);
    pthread_create(&sender, nullptr, send_message, &td);

    pthread_join(recvr, nullptr);
    pthread_join(sender, nullptr);

    close(td.sockfd);
    return 0;
}

Main.cc

#include "UdpServer.hpp"
#include <memory>
#include <cstdio>
#include <vector>

// "159.75.96.122" 点分十进制字符串风格的IP地址。用户习惯这种类型的IP地址
void Usage(std::string proc) {
    std::cout << "\n\tUsage: " << proc << " prot[1024+]\n" << std::endl;
}

// ./udpserver port 命令行启动时传入端口号
int main(int argc, char* argv[]) {
    if (argc != 2) {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port));
    svr->Init();
    svr->Run();
    return 0;
}

云服务器不允许直接bind公有IP,我们也不推荐编写服务器的时候,bind明确的IP,推荐直接写成INADDR_ANY

/* Address to accept any incoming messages. */
#define INADDR_ANY ((in_addr_t) 0x00000000)

在网络编程中,当一个进程需要绑定一个网络端口以进行通信时,可以使用INADDR_ANY作为IP地址参数。这样做意味着该端口可以接受来自任何IP地址的连接请求,无论是本地主机还是远程主机。例如,如果服务器有多个网卡(每个网卡上有不同的IP地址),使用INADDR_ANY可以省去确定数据是从服务器上具体哪个网卡/IP地址上面获取的。

  • UDP协议支持全双工,一个sockfd,既可以读取,又可以写入,对于客户端和服务端同样如此
  • 多线程客户端,同时读取和写入
  • 测试的时候,使用管道进行演示

补充参考内容

地址转换函数

本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位的IP地址
但是我们通常用点分十进制的字符串表示IP地址,以下函数可以在字符串表示和in_addr表示之间转换;
字符串转in_addr的函数:

in_addr转字符串的函数:

其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。

代码示例:

关于inet_ntoa

inet_ntoa这个函数返回了一个char*,很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果。那么是否需要调用者手动释放呢?

man手册上说,inet_ntoa函数,是把这个返回结果放到了静态存储区。这个时候不需要我们手动进行释放。

那么问题来了,如果我们调用多次这个函数,会有什么样的效果呢?参考如下代码:

运行结果如下:

因为inet_ntoa把结果放到自己内部的一个静态存储区,这样第二次调用的结果会覆盖掉上一次的结果。

  • 思考:如果有多个线程调用 inet_ntoa,是否会出现异常情况呢?
  • 在APUE中,明确提出inet_ntoa不是线程安全的函数;
  • 但是在centos7上测试,并没有出现问题,可能内部的实现加了互斥锁;
  • 同学们课后自己写程序验证一下在自己的机器上inet_ntoa是否会出现多线程的问题;
  • 在多线程环境下,推荐使用inet_ntop,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题;

网络命令

Ping 命令

$ ping www.qq.com
$ ping -c 5 www.qq.com
PING ins-r23tsuuf.ias.tencent-cloud.net (121.14.77.221) 56(84) bytes of data.
64 bytes from 121.14.77.221 (121.14.77.221): icmp_seq=1 ttl=48 time=35.1 ms
64 bytes from 121.14.77.221 (121.14.77.221): icmp_seq=2 ttl=48 time=35.1 ms
64 bytes from 121.14.77.221 (121.14.77.221): icmp_seq=3 ttl=48 time=35.1 ms
64 bytes from 121.14.77.221 (121.14.77.221): icmp_seq=4 ttl=48 time=35.1 ms
64 bytes from 121.14.77.221 (121.14.77.221): icmp_seq=5 ttl=48 time=35.1 ms
--- ins-r23tsuuf.ias.tencent-cloud.net ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4005ms

netstat

netstat是一个用来查看网络状态的重要工具。
语法:netstat [选项]
功能:查看网络状态
常用选项

  • n 拒绝显示别名,能显示数字的全部转化成数字
  • l 仅列出有在 Listen (监听) 的服务状态
  • p 显示建立相关链接的程序名
  • t (tcp)仅显示tcp相关选项
  • u (udp)仅显示udp相关选项
  • a (all)显示所有选项,默认不显示LISTEN相关
// 每个1s执⾏⼀次 netstat -nltp
$ watch -n 1 netstat -nltp

pidof

在查看服务器的进程id时非常方便。
语法:pidof [进程名]
功能:通过进程名,查看进程id

$ ps axj | head -1 && ps ajx | grep tcp_server
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
2958169 2958285 2958285 2958169 pts/2 2958285 S+ 1002 0:00
./tcp_server 8888
whb@bite01:~$ pidof tcp_server
2958285

第二章:Socket编程TCP

TCP网络程序

和刚才UDP类似。实现一个简单的英译汉的功能

TCP socket API 详解 

下面介绍程序中用到的socket API,这些函数都在sys/socket.h中。

socket():

  • socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;
  • 应用程序可以像读写文件一样用read/write在网络上传收数据;
  • 如果socket()调用出错则返回-1;
  • 对于IPv4,family参数指定为AF_INET;
  • 对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议
  • protocol参数的介绍从略,指定为0即可。

bind():

  • 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接;服务器需要调用bind绑定一个固定的网络地址和端口号;
  • bind()成功返回0,失败返回-1。
  • bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号;
  • 前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度;

我们的程序中对myaddr参数是这样初始化的:

  1. 将整个结构体清零;
  2. 设置地址类型为AF_INET;
  3. 网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址;
  4. 端口号为SERV_PORT,我们定义为9999;

listen():

  • listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略,这里设置不会太大(一般是5),具体细节同学们课后深入研究;
  • listen()成功返回0,失败返回-1;

accept():

  • 三次握手完成后,服务器调用accept()接受连接;
  • 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
  • addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
  • 如果给addr参数传NULL,表示不关心客户端的地址;
  • addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);

我们的服务器程序结构是这样的:

理解accept的返回值:饭店拉客例子

connect

  • 客户端需要调用connect()连接服务器;
  • connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址;
  • connect()成功返回0,出错返回-1;

V1 - 单进程和多进程版

单进程只能给一个客户端提供服务,一般不用。

log.hpp 同UDP

TcpServer.hpp

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

const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
const int backlog = 5;//一般不要设置的太大
Log lg;

enum {
	UsageError = 1,
	SocketError,
	BindError,
	ListenError
};

class TcpServer {
public:
	TcpServer(const uint16_t& port, const std::string& ip = defaultip)
		: listensockfd_(defaultfd), port_(port), ip_(ip) {
	}

	void InitServer() {
		listensockfd_ = socket(AF_INET, SOCK_STREAM, 0);
		if (listensockfd_ < 0) {
			lg(Fatal, "create socket error, errno:%d, errstring:%s", errno, strerror(errno));
			exit(SocketError);
		}
		lg(Info, "create socket success, listensockfd_:%d", listensockfd_);

		struct sockaddr_in local;
		memset(&local, 0, sizeof(local));
		local.sin_family = AF_INET;
		local.sin_port = htons(port_);
		inet_aton(ip_.c_str(), &local.sin_addr);
		// local.sin_addr.s_addr = INADDR_ANY; //虚拟机使用

		if (bind(listensockfd_, (struct sockaddr*)&local, sizeof(local)) < 0) {
			lg(Fatal, "bind error, errno:%d, errstring:%s", errno, strerror(errno));
			exit(BindError);
		}
		lg(Info, "bind success, listensockfd_:%d", listensockfd_);

		//TCP是面向连接的,服务器一般是比较"被动的"。服务器始终处于一直在等待连接到来的状态
		if (listen(listensockfd_, backlog) < 0) {
			lg(Fatal, "listen error, errno:%d, errstring:%s", errno, strerror(errno));
			exit(ListenError);
		}
		lg(Info, "listen success, listensockfd_:%d", listensockfd_);

	}

	void Start() {
		// signal(SIGCHLD, SIG_IGN);//这种方式可以不用等
		lg(Info, "TCP Server is running...");
		for (;;) {
			//1.获取新链接
			struct sockaddr_in client;
			socklen_t len = sizeof(client);
			//listensockfd_ 用于监听客户端的连接请求,它只是等待新的连接到来,并不直接参与数据的发送和接收。(一般只有一个);
			//sockfd 用于与每个具体的客户端进行数据通信,代表了与某一个客户端的连接,服务器可以通过它来发送和接收数据。(可以有多个)
			int sockfd = accept(listensockfd_, (struct sockaddr*)&client, &len);
			if (sockfd < 0) {
				lg(Warning, "accept error, errno:%d, errstring:%s", errno, strerror(errno));
				continue;//获取失败,继续获取
			}

			// 为什么IP地址和端口号要主机和网络序列互转,这两个字段要写入操作系统,需要自己转。
			// 但我们发的信息为什么不需要?套接字接口会自动转换。
			// 因为使用socket通信,所以需要IP和端口号来标识自身唯一性。所以服务器才能给客户端发消息。
			// 主机上有各种APP,需要不同的端口号。如果固定端口号,可能会造成冲突。
			uint16_t clientport = ntohs(client.sin_port);
			char clientip[32];
			inet_ntop(AF_INET, &client.sin_addr, clientip, sizeof(clientip));

			//2.根据新连接来进行通信
			lg(Info, "get a new link..., sockfd:%d, client ip:%s, client port:%d", sockfd, clientip, clientport);

			// //version 1 -- 单进程版
			// //启动2个客户端,只有第一个能正常发消息。第一个退了以后,第二个客户端的消息才发送。
			// Service(sockfd, clientip, clientport);
			// close(sockfd);

			//version 2 -- 多进程版
			pid_t id = fork();
			if (id == 0) {
				//child
				close(listensockfd_); //子进程会拷贝父进程的文件描述表,关闭不需要的。
				//子进程成功创建孙子进程后退出,这样就能立马被父进程回收。孙子进程被系统领养
				if (fork() > 0) exit(0);
				Service(sockfd, clientip, clientport);
				close(sockfd);//通信结束,关闭该文件描述符。
				exit(0);
			}
			//father
			close(sockfd);//和子进程一样的原因,关闭不需要的。
			//如果父进程阻塞等待就和单进程版本一样,只有子进程退出了,父进程才能获取新连接。所以子进程要创建孙子进程
			pid_t rid = waitpid(id, nullptr, 0);
		}
	}
	void Service(int sockfd, const std::string& clientip, const uint16_t& clientport) {
		//测试代码,发什么回显什么
		char buffer[4096];
		while (true) {
			ssize_t n = read(sockfd, buffer, sizeof(buffer));
			if (n > 0) {
				buffer[n] = 0;
				std::cout << "client say#" << buffer << std::endl;
				std::string echo_string = "TCP Server echo#";
				echo_string += buffer;

				write(sockfd, echo_string.c_str(), echo_string.size());
			}
			else if (n == 0) { //客户端退出
				lg(Info, "%s:%d quit, server close sockfd:%d", clientip.c_str(), clientport, sockfd);
				break;
			}
			else {
				lg(Warning, "read error, sockfd:%d, client ip:%s, client port:%d", sockfd, clientip.c_str(), clientport);
				break;
			}
		}
	}

	~TcpServer() {

	}
private:
	int listensockfd_;
	uint16_t port_;
	std::string ip_;
};

TcpClient.cc

void Usage(std::string proc) {
	std::cout << "\n\tUsage: " << proc << " serverip serverport\n" << std::endl;
}

// ./tcpclient serverip serverport
int main(int argc, char* argv[]) {
	if (argc != 3) {
		Usage(argv[0]);
		return 1;
	}
	std::string serverip = argv[1];
	uint16_t serverport = std::stoi(argv[2]);

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

	struct sockaddr_in server;
	server.sin_family = AF_INET;
	server.sin_port = htons(serverport);
	inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);

	// TCP客户端需一定要绑定,但不需要显示的绑定。系统进行bind,随机端口。
	// 客户端发起connect的时候,进行自动随机bind
	int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server));
	if (n < 0) {
		std::cerr << "connect error" << std::endl;
		return 2;
	}

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

		write(sockfd, message.c_str(), message.size());

		char inbuffer[4096];
		int n = read(sockfd, inbuffer, sizeof(inbuffer));
		if (n > 0) {
			inbuffer[n] = 0;
			std::cout << inbuffer << std::endl;
		}
	}
	close(sockfd);
	return 0;
}

Main.cc

#include "TcpServer.hpp"
#include <iostream>
#include <memory>

void Usage(std::string proc) {
	std::cout << "\n\tUsage: " << proc << " prot[1024+]\n" << std::endl;
}

// ./tcpserver 8080
int main(int argc, char* argv[]) {
	if (argc != 2) {
		Usage(argv[0]);
		exit(UsageError);
	}
	uint16_t port = std::stoi(argv[1]);
	std::unique_ptr<TcpServer> tcp_svr(new TcpServer(port));
	tcp_svr->InitServer();
	tcp_svr->Start();
	return 0;
}

由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。
注意:

  • 客户端不是不允许调用bind(),只是没有必要显式地调用bind()固定一个端口号。否则如果在同一台机器上启动多个客户端,就会出现端口号被占用导致不能正确建立连接;
  • 服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦;

测试多个连接的情况
再启动一个客户端,尝试连接服务器,发现第二个客户端,不能正确的和服务器进行通信。
分析原因,是因为我们accept了一个请求之后,就在一直while循环尝试read,没有继续调用到accept,导致不能接受新的请求。
我们当前的这个TCP,只能处理一个连接,这是不科学的。

V2 - 多线程版

TcpServer.hpp

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

 //version 3 -- 多线程版
const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
const int backlog = 5;//一般不要设置的太大
Log lg;

enum {
	UsageError = 1,
	SocketError,
	BindError,
	ListenError
};

class TcpServer;

class ThreadData {
public:
	ThreadData(int fd, const std::string& ip, const uint16_t& p, TcpServer* t)
		: sockfd(fd), clientip(ip), clientport(p), tsvr(t) {
	}
public:
	int sockfd;
	std::string clientip;
	uint16_t clientport;
	TcpServer* tsvr;
};

class TcpServer {
public:
	TcpServer(const uint16_t& port, const std::string& ip = defaultip)
		: listensockfd_(defaultfd), port_(port), ip_(ip) {
	}

	void InitServer() {
		listensockfd_ = socket(AF_INET, SOCK_STREAM, 0);
		if (listensockfd_ < 0) {
			lg(Fatal, "create socket error, errno:%d, errstring:%s", errno, strerror(errno));
			exit(SocketError);
		}
		lg(Info, "create socket success, listensockfd_:%d", listensockfd_);

		struct sockaddr_in local;
		memset(&local, 0, sizeof(local));
		local.sin_family = AF_INET;
		local.sin_port = htons(port_);
		inet_aton(ip_.c_str(), &local.sin_addr);
		// local.sin_addr.s_addr = INADDR_ANY; //虚拟机使用

		if (bind(listensockfd_, (struct sockaddr*)&local, sizeof(local)) < 0) {
			lg(Fatal, "bind error, errno:%d, errstring:%s", errno, strerror(errno));
			exit(BindError);
		}
		lg(Info, "bind success, listensockfd_:%d", listensockfd_);

		//TCP是面向连接的,服务器一般是比较"被动的"。服务器始终处于一直在等待连接到来的状态
		if (listen(listensockfd_, backlog) < 0) {
			lg(Fatal, "listen error, errno:%d, errstring:%s", errno, strerror(errno));
			exit(ListenError);
		}
		lg(Info, "listen success, listensockfd_:%d", listensockfd_);

	}

	static void* Routine(void* args) { //成员函数还有this参数,和线程执行函数只有一个参数的规则不符,所以加static修饰
		//如果Start函数中等待线程退出,那么无法并行。所以线程分离,运行完自动回收资源并退出。
		pthread_detach(pthread_self());
		ThreadData* td = static_cast<ThreadData*>(args);
		td->tsvr->Service(td->sockfd, td->clientip, td->clientport);
		delete td;
		return nullptr;
	}
	void Start() {
		// signal(SIGCHLD, SIG_IGN);//这种方式可以不用等
		lg(Info, "TCP Server is running...");
		for (;;) {
			//1.获取新链接
			struct sockaddr_in client;
			socklen_t len = sizeof(client);
			//listensockfd_ 用于监听客户端的连接请求,它只是等待新的连接到来,并不直接参与数据的发送和接收。(一般只有一个);
			//sockfd 用于与每个具体的客户端进行数据通信,代表了与某一个客户端的连接,服务器可以通过它来发送和接收数据。(可以有多个)
			int sockfd = accept(listensockfd_, (struct sockaddr*)&client, &len);
			if (sockfd < 0) {
				lg(Warning, "accept error, errno:%d, errstring:%s", errno, strerror(errno));
				continue;//获取失败,继续获取
			}

			// 为什么IP地址和端口号要主机和网络序列互转,这两个字段要写入操作系统,需要自己转。
			// 但我们发的信息为什么不需要?套接字接口会自动转换。
			// 因为使用socket通信,所以需要IP和端口号来标识自身唯一性。所以服务器才能给客户端发消息。
			// 主机上有各种APP,需要不同的端口号。如果固定端口号,可能会造成冲突。
			uint16_t clientport = ntohs(client.sin_port);
			char clientip[32];
			inet_ntop(AF_INET, &client.sin_addr, clientip, sizeof(clientip));

			//2.根据新连接来进行通信
			// 每有一个客户,就要创建一个线程。目前的Service是死循环,客户不退线程越来越多。来了连接才创建线程。
			lg(Info, "get a new link..., sockfd:%d, client ip:%s, client port:%d", sockfd, clientip, clientport);
			ThreadData* td = new ThreadData(sockfd, clientip, clientport, this);
			pthread_t pid;
			pthread_create(&pid, nullptr, Routine, td);
		}
	}
	void Service(int sockfd, const std::string& clientip, const uint16_t& clientport) {
		//测试代码,发什么回显什么
		char buffer[4096];
		while (true) {
			ssize_t n = read(sockfd, buffer, sizeof(buffer));
			if (n > 0) {
				buffer[n] = 0;
				std::cout << "client say#" << buffer << std::endl;
				std::string echo_string = "TCP Server echo#";
				echo_string += buffer;

				write(sockfd, echo_string.c_str(), echo_string.size());
			}
			else if (n == 0) { //客户端退出
				lg(Info, "%s:%d quit, server close sockfd:%d", clientip.c_str(), clientport, sockfd);
				break;
			}
			else {
				lg(Warning, "read error, sockfd:%d, client ip:%s, client port:%d", sockfd, clientip.c_str(), clientport);
				break;
			}
		}
	}

	~TcpServer() {

	}
private:
	int listensockfd_;
	uint16_t port_;
	std::string ip_;
};

TcpClient.cc 同上

void Usage(std::string proc) {
	std::cout << "\n\tUsage: " << proc << " serverip serverport\n" << std::endl;
}

// ./tcpclient serverip serverport
int main(int argc, char* argv[]) {
	if (argc != 3) {
		Usage(argv[0]);
		return 1;
	}
	std::string serverip = argv[1];
	uint16_t serverport = std::stoi(argv[2]);

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

	struct sockaddr_in server;
	server.sin_family = AF_INET;
	server.sin_port = htons(serverport);
	inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);

	// TCP客户端需一定要绑定,但不需要显示的绑定。系统进行bind,随机端口。
	// 客户端发起connect的时候,进行自动随机bind
	int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server));
	if (n < 0) {
		std::cerr << "connect error" << std::endl;
		return 2;
	}

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

		write(sockfd, message.c_str(), message.size());

		char inbuffer[4096];
		int n = read(sockfd, inbuffer, sizeof(inbuffer));
		if (n > 0) {
			inbuffer[n] = 0;
			std::cout << inbuffer << std::endl;
		}
	}
	close(sockfd);
	return 0;
}

Main.cc 同上

#include "TcpServer.hpp"
#include <iostream>
#include <memory>

void Usage(std::string proc) {
	std::cout << "\n\tUsage: " << proc << " prot[1024+]\n" << std::endl;
}

// ./tcpserver 8080
int main(int argc, char* argv[]) {
	if (argc != 2) {
		Usage(argv[0]);
		exit(UsageError);
	}
	uint16_t port = std::stoi(argv[1]);
	std::unique_ptr<TcpServer> tcp_svr(new TcpServer(port));
	tcp_svr->InitServer();
	tcp_svr->Start();
	return 0;
}

V3 - 线程池版(守护进程)

Init.hpp

#pragma once

#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "log.hpp"

const std::string dictname = "./dict.txt";
const std::string sep = ":";

static bool Split(const std::string& s, std::string* part1, std::string* part2) {
    auto pos = s.find(sep);
    if (pos == std::string::npos) return false;
    *part1 = s.substr(0, pos);
    *part2 = s.substr(pos+1);//pos指向冒号,要跳过
    return true;
}

class Init {
public:
    Init() {
        // ifstream 是 C++ 标准库中的一个类,用来从文件中读取数据。
        // 它是文件流类的一部分,属于 <fstream> 头文件。
        // ifstream 的全称是 input file stream,即“输入文件流”,它提供了文件读取的功能。
        // 用法:
        // 打开文件:通过 ifstream 对象打开一个文件进行读取。
        // 读取文件内容:可以使用流操作符 >> 或者 getline 来从文件中读取数据。
        // 检查文件是否成功打开:通常通过 is_open() 函数检查文件是否成功打开。
        // 关闭文件:文件流会在销毁时自动关闭,但也可以手动调用 close()。
        std::ifstream in(dictname);
        if (!in.is_open()) {
            lg(Fatal, "ifstream open %s error", dictname.c_str());
            exit(1);
        }
        std::string line;
        while (std::getline(in, line)) {  // 按行读取文件内容
            std::string part1, part2;
            Split(line, &part1, &part2);
            dict.insert({part1, part2});
        }
    }
    
    std::string translation(const std::string& key) {
        auto iter = dict.find(key);
        if (iter == dict.end()) return "Unkonw";
        else return iter->second;
    }

private:
    std::unordered_map<std::string, std::string> dict;
};

Task.hpp

#pragma once

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

extern Log lg;
Init init;

class Task {
public:
    Task() {}
    Task(int sockfd, const std::string& clientip, const uint16_t& clientport) 
        :sockfd_(sockfd), clientip_(clientip), clientport_(clientport) {
    }

    void run() {
        char buffer[4096];
        ssize_t n = read(sockfd_, buffer, sizeof(buffer));
        if (n > 0) {
            // buffer[n-2] = 0;//使用telnet最后需要按回车还有换行符,去掉回车换行等
            buffer[n] = 0;
            std::cout << "client key# " << buffer << std::endl;
            std::string echo_string = init.translation(buffer);

            //假设服务器正准备写,客户端就关闭连接了。此时可能会造成类似管道的情况,
            //写入返回0,OS就结束写的这个进程。但服务器不能随便关闭,所以要对写入做判断
            //而且还需要处理SIG_PIPE信号,忽略处理
            n = write(sockfd_, echo_string.c_str(), echo_string.size());
            if (n < 0) {
                lg(Warning, "write error, errno:%d, errstring:%s", errno, strerror(errno));
            }
        }
        else if (n == 0) { //客户端退出
            lg(Info, "%s:%d quit, server close sockfd:%d", clientip_.c_str(), clientport_, sockfd_);
        }
        else {
            lg(Warning, "read error, sockfd:%d, client ip:%s, client port:%d", sockfd_, clientip_.c_str(), clientport_);
        }
        close(sockfd_);
    }

    void operator()() { run(); }


    ~Task() {}
private:
    int sockfd_;
    std::string clientip_;
    uint16_t clientport_;
};

ThreadPool.hpp

#pragma once

#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>

using namespace std;

struct ThreadInfo { //封装线程ID和名字
    pthread_t tid;
    string name;
};

static const int defaultnum = 5;

template <class T>
class ThreadPool {
public:
    void Lock() { pthread_mutex_lock(&mutex_); }
    void Unlock() { pthread_mutex_unlock(&mutex_); }
    void Weakup() { pthread_cond_signal(&cond_); }
    void ThreadSleep() { pthread_cond_wait(&cond_, &mutex_); }
    bool IsQueueEmpty() { return tasks_.empty(); }
    string GetThreadName(pthread_t tid) {
        for (const auto& ti : threads_)
            if (ti.tid == tid)
                return ti.name;
        return "None";
    }
public:

    //如果将线程执行函数放在类内,那么类的成员函数还会有个隐藏的this指针参数。所以参数个数不对
    // void* HandlerTask(void* args) { 
    static void* HandlerTask(void* args) { //加static就没有this指针参数
    ThreadPool<T>* tp = static_cast<ThreadPool<T>*>(args);
    string name = tp->GetThreadName(pthread_self());
        while (true) {
            tp->Lock();
            while (tp->IsQueueEmpty()) {
                tp->ThreadSleep();
            }
            T t = tp->Pop();
            tp->Unlock();
            t();
            // cout << name << " run, result: " << t.GetResult() << endl;
        }
    }

    void Start() {
        int num = threads_.size();
        for (int i = 0; i < num; i++) {
            threads_[i].name = "thread-" + to_string(i+1);
            // pthread_create(&threads_[i].tid, nullptr, HandlerTask, nullptr);//编译不通过,因为HandlerTask函数是静态成员函数
            pthread_create(&threads_[i].tid, nullptr, HandlerTask, this);//传当前线程池对象(ThreadPool实例)的地址
        }
    }

    T Pop() {
        T t = tasks_.front();
        tasks_.pop();
        return t;
    }

    //如果先解锁再唤醒。可能没有现成在等导致信号“丢失”。那线程就可能永远沉睡,再也没人通知它有任务来
    void Push(const T& t) {
        Lock();
        tasks_.push(t);
        Weakup();
        Unlock();
    }
    // //只在第一次创建单例对象时有并发问题,需要保护。
    // //如果对申请单例加锁,那么往后的线程都要经历申请锁->判断->释放锁。(即后续线程都要串行申请单例)
    // static ThreadPool<T>* GetInstance() {
    //     pthread_mutex_lock(&lock_);
    //     if (nullptr == tp_) {
    //         cout << "log: singleton create done first!" << endl;
    //         tp_ = new ThreadPool<T>;
    //     }
    //     pthread_mutex_unlock(&lock_);
    //     return tp_;
    // }
    static ThreadPool<T>* GetInstance() {
        // 外层检查:如果单例已存在,无需加锁,直接返回,减少锁竞争
        if (nullptr == tp_) { //开关,可以并发获取单例(第一次检查:避免已创建后仍频繁加锁,提高性能)
            pthread_mutex_lock(&lock_);
            if (nullptr == tp_) { //判断是否是单例
                cout << "log: singleton create done first!" << endl;
                tp_ = new ThreadPool<T>;
            }
            pthread_mutex_unlock(&lock_);
        }
        return tp_;
    }
private:
    ThreadPool(int num = defaultnum)
        :threads_(num) {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    } 
    ~ThreadPool() {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }
    ThreadPool(const ThreadPool<T>&) = delete; 
    ThreadPool<T>& operator=(const ThreadPool<T>&) = delete;  
private:
    vector<ThreadInfo> threads_;//将封装的线程结构体放入vector
    queue<T> tasks_; //任务队列
    pthread_mutex_t mutex_;
    pthread_cond_t cond_;
    static ThreadPool<T>* tp_;
    static pthread_mutex_t lock_;//获取单例时需要加锁
};
template <class T>
ThreadPool<T>* ThreadPool<T>::tp_ = nullptr;
template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;

Daemon.hpp

#pragma once

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

const std::string nullfile = "/dev/null"; //进行垃圾处理的字符文件

void Daemon(const std::string& cwd = "") {
    //1.忽略其他异常信号
    signal(SIGCLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGSTOP, SIG_IGN);

    //将自己变成独立的会话
    if (fork() > 0) exit(0);
    setsid();

    //3.更改当前调用进程的工作目录
    // 守护进程对外提供服务,可能会把头文件等写到系统中,所以工作目录可能会在根目录
    if (!cwd.empty()) chdir(cwd.c_str());

    //4.标准输入,标准输出,标准错误重定向至 /dev/null
    // 守护进程不应该在向标准输出打印。如果关闭0 1 2文件描述符,那么所有打印就会报错。
    // /dev/null 垃圾桶。里面什么都读取不到,向里面写入也会被丢弃
    int fd = open(nullfile.c_str(), O_RDWR);
    if (fd > 0) {
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
    }
}

TcpServer.hpp

#pragma once

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

extern Log lg;

//version 4 -- 线程池版
//提前预先创建线程,不提供长服务。限定线程上线,把客户的访问作为任务
const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
const int backlog = 5;//一般不要设置的太大

enum {
    UsageError = 1,
    SocketError,
    BindError,
    ListenError 
};

class TcpServer {
public:
    TcpServer(const uint16_t& port, const std::string& ip = defaultip) 
        : listensockfd_(defaultfd), port_(port), ip_(ip) {
    }

    void InitServer() {
        listensockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (listensockfd_ < 0) {
            lg(Fatal, "create socket error, errno:%d, errstring:%s", errno, strerror(errno));
            exit(SocketError);
        }
        lg(Info, "create socket success, listensockfd_:%d", listensockfd_);
        
        //防止偶发性的服务器无法立即重启
        int opt = 1;
        setsockopt(listensockfd_, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));

        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);
        inet_aton(ip_.c_str(), &local.sin_addr);
        // local.sin_addr.s_addr = INADDR_ANY; //虚拟机使用

        if (bind(listensockfd_, (struct sockaddr*)&local, sizeof(local)) < 0) {
            lg(Fatal, "bind error, errno:%d, errstring:%s", errno, strerror(errno));
            exit(BindError);
        }
        lg(Info, "bind success, listensockfd_:%d", listensockfd_);

        //TCP是面向连接的,服务器一般是比较"被动的"。服务器始终处于一直在等待连接到来的状态
        if (listen(listensockfd_, backlog) < 0) {
            lg(Fatal, "listen error, errno:%d, errstring:%s", errno, strerror(errno));
            exit(ListenError);
        }
        lg(Info, "listen success, listensockfd_:%d", listensockfd_);

    }

    void Start() {
        Daemon();
        // signal(SIGPIPE, SIG_IGN);//看Task.hpp run函数中写入的解释
        ThreadPool<Task>::GetInstance()->Start();//获取线程池单例
        // signal(SIGCHLD, SIG_IGN);//这种方式也可以不用等线程退出
        lg(Info, "TCP Server is running...");
        for(;;) {
            //1.获取新链接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            //listensockfd_ 用于监听客户端的连接请求,它只是等待新的连接到来,并不直接参与数据的发送和接收。(一般只有一个);
            //sockfd 用于与每个具体的客户端进行数据通信,代表了与某一个客户端的连接,服务器可以通过它来发送和接收数据。(可以有多个)
            int sockfd = accept(listensockfd_, (struct sockaddr*)&client, &len);
            if (sockfd < 0) {
                lg(Warning, "accept error, errno:%d, errstring:%s", errno, strerror(errno));
                continue;//获取失败,继续获取
            }

            // 为什么IP地址和端口号要主机和网络序列互转,这两个字段要写入操作系统,需要自己转。
            // 但我们发的信息为什么不需要?套接字接口会自动转换。
            // 因为使用socket通信,所以需要IP和端口号来标识自身唯一性。所以服务器才能给客户端发消息。
            // 主机上有各种APP,需要不同的端口号。如果固定端口号,可能会造成冲突。
            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET, &client.sin_addr, clientip, sizeof(clientip));
            
            //2.根据新连接来进行通信
            // 每有一个客户,就要创建一个线程。目前的Service是死循环,客户不退线程越来越多。来了连接才创建线程。
            lg(Info, "get a new link..., sockfd:%d, client ip:%s, client port:%d", sockfd, clientip, clientport);
            Task t(sockfd, clientip, clientport);
            ThreadPool<Task>::GetInstance()->Push(t);

        }
    }

    ~TcpServer() {}
private:
    int listensockfd_;
    uint16_t port_;
    std::string ip_;
};

TcpClient.cc 可断线重连

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

//客户端多次连接
void Usage(std::string proc) {
    std::cout << "\n\tUsage: " << proc << " serverip serverport\n" << std::endl;
}

// ./tcpclient serverip serverport
int main(int argc, char* argv[]) {
    if (argc != 3) {
        Usage(argv[0]);
        return 1;
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);
    // TCP客户端一定要绑定,但不需要显示的绑定。系统进行bind,随机端口。
    // 客户端发起connect的时候,进行自动随机bind
    
    while (true) { 
        //连接管理
        int sockfd = -1;
        int cnt = 5; //重连次数
        bool isreconnect = false;

        sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd <  0) {
            std::cerr << "socket error" << std::endl;
        }
        do {
            //服务器关闭连接也关闭了套接字(socket),即客户端的资源被释放,而发起连接要创建套接字。
            //所以while循环要移到上面创建套接字的部分
            // while (true) {  
            int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server));
            if (n < 0) {
                isreconnect = true;
                cnt--;
                std::cerr << "connect error, reconnect:" << cnt << std::endl;
                close(sockfd);
                sleep(1);
            }
            else break;
        } while (cnt && isreconnect);
        
        if (cnt == 0) {
            std::cerr << "user offline" << std::endl;
            break;
        }

        //提供服务
        // while (true) { //可以设置为一直提供服务
        std::string message;
        std::cout << "Please Enter:";
        getline(std::cin, message);
        int n = write(sockfd, message.c_str(), message.size());
        if (n < 0) 
            std::cerr << "write error" << std::endl;

        char inbuffer[4096];
        n = read(sockfd, inbuffer, sizeof(inbuffer));
        if (n > 0) {
            inbuffer[n] = 0;
            std::cout << inbuffer << std::endl;
        }
        close(sockfd);
        // }
    }
    
    return 0;
}

Main.cc

#include "TcpServer.hpp"
#include <iostream>
#include <memory>

void Usage(std::string proc) {
    std::cout << "\n\tUsage: " << proc << " prot[1024+]\n" << std::endl;
}

// ./tcpserver 8080
int main(int argc, char* argv[]) {
    if (argc != 2) {
        Usage(argv[0]);
        exit(UsageError);
    }
    uint16_t port = std::stoi(argv[1]);
    lg.Enable(Classfile);//日志功能
    std::unique_ptr<TcpServer> tcp_svr(new TcpServer(port));
    tcp_svr->InitServer();
    tcp_svr->Start();    
    return 0;
}

第三章:进程间关系与守护进程

1. 进程组

1-1 什么是进程组

之前我们提到了进程的概念,其实每一个进程除了有一个进程ID(PID)之外还属于一个进程组。进程组是一个或者多个进程的集合,一个进程组可以包含多个进程。每一个进程组也有一个唯一的进程组ID(PGID),并且这个PGID类似于进程ID,同样是一个正整数,可以存放在pid_t数据类型中。

$ ps -eo pid,pgid,ppid,comm | grep test
#结果如下
PID  PGID PPID COMMAND
2830 2830 2259 test
# -e 选项表示every的意思,表示输出每一个进程信息
# -o 选项以逗号操作符(,)作为定界符,可以指定要输出的列

1-2 组长进程

每一个进程组都有一个组长进程。组长进程的ID等于其进程ID。我们可以通过ps命令看到组长进程的现象:

[node@localhost code]$ ps -o pid,pgid,ppid,comm | cat
# 输出结果
PID  PGID PPID COMMAND
2806 2806 2805 bash
2880 2880 2806 ps
2881 2880 2806 cat

从结果上看ps进程的PID和PGID相同,那也就是说明ps进程是该进程组的组长进程,该进程组包括ps和cat两个进程。

  • 进程组组长的作用:进程组组长可以创建一个进程组或者创建该组中的进程
  • 进程组的生命周期:从进程组创建开始到其中最后一个进程离开为止。注意:只要某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否已经终止无关。

2. 会话

2-1 什么是会话

刚刚我们谈到了进程组的概念,那么会话又是什么呢?会话其实和进程组息息相关,会话可以看成是一个或多个进程组的集合,一个会话可以包含多个进程组。每一个会话也有一个会话ID(SID)

通常我们都是使用管道将几个进程编成一个进程组。如上图的进程组2和进程组3可能是由下列命令形成的:

[node@localhost code]$ proc2 | proc3 &
[node@localhost code]$ proc4 | proc5 | proc6 &
# &表示将进程组放在后台执行

我们举一个例子观察一下这个现象:

# 用管道和sleep组成一个进程组放在后台运行
[node@localhost code]$ sleep 100 | sleep 200 | sleep 300 &
# 查看ps命令打出来的列描述信息
[node@localhost code]$ ps axj | head -n1
# 过滤sleep相关的进程信息
[node@localhost code]$ ps axj | grep sleep | grep -v grep
# a选项表示不仅列当前用户的进程,也列出所有其他用户的进程
# x选项表示不仅列有控制终端的进程,也列出所有无控制终端的进程
# j选项表示列出与作业控制相关的信息,作业控制后续会讲
# grep的-v选项表示反向过滤,即不过滤带有grep字段相关的进程

# 结果如下
PPID PID  PGID SID  TTY   TPGID STAT UID  TIME COMMAND
2806 4223 4223 2780 pts/2 4229  S    1000 0:00 sleep 100
2806 4224 4223 2780 pts/2 4229  S    1000 0:00 sleep 200
2806 4225 4223 2780 pts/2 4229  S    1000 0:00 sleep 300

2-2 如何创建会话

可以调用setsid函数来创建一个会话,前提是调用进程不能是一个进程组的组长。

#include <unistd.h>
/*
*功能:创建会话
*返回值:创建成功返回SID, 失败返回-1
*/
pid_t setsid(void);

该接口调用之后会发生:

  • 调用进程会变成新会话的会话首进程。此时,新会话中只有唯一的一个进程
  • 调用进程会变成进程组组长。新进程组ID就是当前调用进程ID
  • 该进程没有控制终端。如果在调用setsid之前该进程存在控制终端,则调用之后会切断联系

需要注意的是:这个接口如果调用进程原来是进程组组长,则会报错,为了避免这种情况,我们通常的使用方法是先调用fork创建子进程,父进程终止,子进程继续执行,因为子进程会继承父进程的进程组ID,而进程ID则是新分配的,就不会出现错误的情况。

2-3 会话ID(SID)

上边我们提到了会话ID,那么会话ID是什么呢?我们可以先说⼀下会话首进程,会话首进程是具有唯一进程ID的单个进程,那么我们可以将会话首进程的进程ID当做是会话ID。注意:会话ID在有些地方也被称为会话首进程的进程组ID,因为会话首进程总是一个进程组的组长进程,所以两者是等价的。

3. 控制终端

先说一下什么是控制终端?
在UNIX系统中,用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell进程的控制终端。控制终端是保存在PCB中的信息,我们知道fork进程会复制PCB中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端。默认情况下没有重定向,每个进程的标准输入、标准输出和标准错误都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。另外会话、进程组以及控制终端还有一些其他的关系,我们在下边详细介绍一下:

  • 一个会话可以有一个控制终端,通常会话首进程打开一个终端(终端设备或伪终端设备)后,该终端就成为该会话的控制终端。
  • 建立与控制终端连接的会话首进程被称为控制进程。
  • 一个会话中的几个进程组可被分成一个前台进程组以及一个或者多个后台进程组。
  • 如果一个会话有一个控制终端,则它有一个前台进程组,会话中的其他进程组则为后台进程组。
  • 无论何时键入终端的中断键(ctrl+c)或退出键(ctrl+\),就会将中断信号发送给前台进程组的所有进程。
  • 如果终端接口检测到调制解调器(或网络)已经断开,则将挂断信号发送给控制进程(会话首进程)。

这些特性的关系如下图所示:

4. 作业控制

4-1 什么是作业(job)和作业控制(Job Control)?

作业是针对用户来讲,用户完成某项任务而启动的进程,一个作业既可以只包含一个进程,也可以包含多个进程,进程之间互相协作完成任务,通常是一个进程管道。

Shell分前后台来控制的不是进程而是作业或者进程组。一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell可以同时运行一个前台作业和任意多个后台作业,这称为作业控制。

例如下列命令就是一个作业,它包括两个命令,在执行时Shell将在前台启动由两个进程组成的作业:

[node@localhost code]$ cat /etc/filesystems | head -n 5

运行结果如下所示:

xfs
ext4
ext3
ext2
nodev proc

4-2 作业号

放在后台执行的程序或命令称为后台命令,可以在命令的后面加上 & 符号从而让Shell识别这是一个后台命令,后台命令不用等待该命令执行完成,就可立即接收新的命令,另外后台进程执行完后会返回一个作业号以及一个进程号(PID)。

例如下面的命令在后台启动了一个作业,该作业由两个进程组成,两个进程都在后台运行:

[node@localhost code]$ cat /etc/filesystems | grep ext &

执行结果如下:

[1] 2202
ext4
ext3
ext2
# 按下回车
[1]+ 完成 cat /etc/filesystems | grep --color=auto ext
  • 第一行表示作业号和进程ID,可以看到作业号是1,进程ID是2202
  • 第3-4行表示该程序运行的结果,过滤 /etc/filesystems 有关 ext 的内容
  • 第6行分别表示作业号、默认作业、作业状态以及所执行的命令

关于默认作业:对于一个用户来说,只能有一个默认作业(+),同时也只能有一个即将成为默认作业的作业(-),当默认作业退出后,该作业会成为默认作业。

  • + : 表示该作业号是默认作业
  • - :表示该作业即将成为默认作业
  • 无符号:表示其他作业

4-3 作业状态

常见的作业状态如下表所示:

4-4 作业的挂起与切回

(1) 作业挂起
我们在执行某个作业时,可以通过 Ctrl+Z 键将该作业挂起,然后Shell会显示相关的作业号、状态以及所执行的命令信息。
例如我们运行一个死循环的程序,通过 Ctrl+Z 将该作业挂起,观察一下对应的作业状态:

#include <stdio.h>
int main() {
	while (1) {
		printf("hello\n");
	}
	return 0;
}

下面我运行这个程序,通过 Ctrl+Z 将该作业挂起:

# 运行可执行程序
[node@localhost code]$ ./test
#键入Ctrl + Z观察现象

运行结果如下:

# 结果依次对应作业号 默认作业 作业状态 运行程序信息
[1]+ 已停止 ./test

可以发现通过 Ctrl+Z 将作业挂起, 该作业状态已经变为了停止状态

(2) 作业切回
如果想将挂起的作业切回,可以通过 fg 命令,fg 后面可以跟作业号或作业的命令名称。如果参数缺省则会默认将作业号为1的作业切到前台来执行,若当前系统只有一个作业在后台进行,则可以直接使用fg命令不带参数直接切回。具体的参数参考如下:

例如我们把刚刚挂起来的 ./test 作业切回到前台:

[node@localhost code]$ fg %%

运行结果为开始无限循环打印 hello,可以发现该作业已经切换到前台了。
注意:当通过 fg 命令切回作业时,若没有指定作业参数,此时会将默认作业切到前台执行,即带有"+"的作业号的作业

4-5 查看后台执行或挂起的作业

我们可以直接通过输入 jobs 命令查看本用户当前后台执行或挂起的作业

  • 参数 -l 则显示作业的详细信息
  • 参数 -p 则只显示作业的PID

例如,我们先在后台及前台运行两个作业,并将前台作业挂起,来用 jobs 命令查看作业相关的信息:

# 在后台运行一个作业sleep
[node@localhost code]$ sleep 300 &
# 运行刚才的死循环可执行程序
[node@localhost code]$ ./test
# 键入Ctrl + Z 挂起作业
# 使用jobs命令查看后台及挂起的作业
[node@localhost code]$ jobs -l

运行结果如下所示:

# 结果依次对应作业号 默认作业 作业状态 运行程序信息
[1]- 2265 运行中 sleep 300 &
[2]+ 2267 停止 ./test

4-6 作业控制相关的信号

上面我们提到了键入 Ctrl + Z 可以将前台作业挂起,实际上是将 SIGTSTP 信号发送至前台进程组作业中的所有进程,后台进程组中的作业不受影响。在unix系统中,存在3个特殊字符可以使得终端驱动程序产生信号,并将信号发送至前台进程组作业,它们分别是:

  • Ctrl + C :中断字符,会产生 SIGINT 信号
  • Ctrl + \ :退出字符,会产生 SIGQUIT 信号
  • Ctrl + Z :挂起字符,会产生 SIGTSTP 信号

终端的I/O(即标准输入和标准输出)和终端产生的信号总是从前台进程组作业连接到实际终端。我们可以通过实验来看到作业控制的功能:

板书

作业

1. 下列有关Socket的说法,错误的是()

A.Socket用于描述IP地址和端口,是一个通信链的句柄
B.Socket通信必须建立连接
C.Socket客户端的端口是不固定的
D.Socket服务端的端口是固定的

答案:B
答案解析:
A正确:概念性理解,socket就是一条通信的句柄
B错误:socket 可以基于TCP 面向连接 也可以基于UDP无连接
C正确:客户端的端口我们推荐是不主动绑定策略,这样可以尽可能的避免端口冲突,让系统选择合适端口绑定,因此不固定
D正确:服务端的端口必须是固定的,因为总是客户端先请求服务端,因此必须提前获知服务端地址端口信息,但是一旦服务器端端口改变,会造成之前的客户端的信息失效找不到服务端了

2. 【多选题】Socket,即套接字,是一个对 TCP / IP协议进行封装 的编程调用接口。socket的使用类型主要有()

A.基于TCP协议,采用流方式,提供可靠的字节流服务
B.基于IP协议,采用流数据提供数据网络发送服务
C.基于HTTP协议,采用数据包方式提供可靠的数据包装服务
D.基于UDP协议,采用数据报方式提供数据打包发送服务

答案:AD
答案解析:
TCP协议,采用流方式,SOCK_STREAM, 可靠
UDP协议,采用数据报方式,SOCK_DGRAM, 不可靠
HTTP协议是应用层协议,在传输层基于TCP协议实现, (可靠是TCP提供的,TCP提供字节流传输)
IP协议是网络层协议,TCP和UDP协议在网络层都是基于IP协议的。(实现数据报传输)
正确选项为:AD

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值