Linux知识点 -- 网络编程套接字

Linux知识点 -- 网络编程套接字

文章目录


一、预备知识

1.认识端口号

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

  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理;
  • IP地址+端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用;
  • 源端口号就是发送数据的端口,目的端口号就是接收数据的端口;

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

在平常使用的APP上,客户端软件发出的请求,通过网络传输到服务端软件进行处理;
真正的网络间通信,本质其实是进程间通信;
将数据在主机间转发仅仅是手段,机器收到之后,需要将数据交付给指定的进程;

2.套接字

IP地址 + 端口号就是套接字;

3.TCP协议与UDP协议

TCP协议:

  • 传输层协议
  • 有连接(可理解为打电话,对方必须响应)
  • 可靠传输(为保证可靠性,需要更多的策略)
  • 面向字节流

UDP协议:

  • 传输层协议
  • 无连接(写信或发邮件)
  • 不可靠传输(使用成本更低;适合直播、视频网站)
  • 面向数据报

4.网络字节序

内存中的数据存储分大端和小端,因此网络通信中不同的主机也会有不同的大小端主机之间通信;
网络规定:所有网络数据,都必须是大端;

网络字节序和主机字节序的转换库函数:

其中:

  • h表示host,n表示network,I表示32位长整数,s表示16位短整数;
  • 例如htonI表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送;
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回;

二、socket编程接口

1.socket常见API

2.sockaddr结构

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

常见的套接字类型:

  • 域间socket
  • 原始socket
  • 网络socket

理论上是三种场景,对应三套接口;

但实际上所有的地址接口都是统一的;

所有接口传入的都是struct sockaddr这个类型的地址参数;

网络和域间套接字的前两个字节是不同的,在函数内部会进行判断;

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址;
  • IPv4、IPv6地址类型分别定义为常数AF_ INET、AF_INET6;这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容
  • socket API可以都用struct sockaddr*类型表示,在使用的时候需要强制转化成sockaddr_in ;这样的好处是程序的通用性,可以接收IPv4, IPv6,以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;

三、UDP套接字编程

1.直接打印客户端信息

udp_server.hpp

  • socket接口:创建套接字

    创建套接字,建立一个通信的一端
    创建成功,返回文件描述符 ,但是UDP是非字节流的操作,不能使用之前的文件接口访问;失败返回-1,并设置errno;
    domain:域,套接字的类型;
    AF_INET是网络通信
    AF_LOCAL是本地通信

    type:套接字通信种类,UDP是面向数据报,使用SOCK_DGRAM;
    SOCK_DGRAM:用户数据报

    protocol:前两个参数确定,这个参数也确定了,一般写为0;

  • bind接口:绑定套接字

    将用户设置的ip和port在内核中和我们当前的进程强关联;
    成功返回0,失败返回-1,设置errno;
    sockfd:套接字;
    sockaddr:通用addr结构,AF_INET网络通信使用sockaddr_in地址结构;
    要显示出来sockaddr_in类型,需包含头文件;

    sockaddr_in结构体:


    其中,sin_port为端口号,sin_addr为ip地址,还需要设置一个sin_family成员,与套接字的类型一致;

  • IP地址格式转换
    "192.168.110.132" -> 点分十进制字符串风格的IP地址,每一个区域取值范围是0-255: 1字节 -> 4个区域;
    理论上,表示一个IP地址,其实4字节就够了;
    需要将点分十进制字符串风格的IP地址 <-> 4字节;
    Sin_addr其实是一个整数类型;

    bzero接口:将指定长度的空间全部置0;
    先要将点分十进制字符串风格的IP地址 -> 4字节,再将4字节主机序列 -> 网络序列;
    有一套接口,可以一次帮我们做完这两件事情, 让服务器在工作过程中,可以从任意IP中获取数据;

    其中,inet_addr接口就是将字符串IP地址转换为网络序列IP地址;

  • recvfrom:从网络中读取数据

    buf:数据存储缓冲区
    len:缓冲区大小
    flags:读取方式,默认0为阻塞方式
    src_addr:输出型参数,拿到数据发送方的ip和port
    addrlen:输入输出型参数;输入: src_addr 缓冲区大小;输出: 实际读到的src_addr大小

  • 网络IP转主机IP

  • sendto:向目标主机发送数据

    dest_addr:目的主机地址
    addrlen:dest_addr的大小

  • 本地环回:127.0.0.1
    client和server发送数据只在本地协议栈中进行数据流动,不会把我们的数据发送到网络中,用于本地服务器的测试;

  • 服务器IP
    云服务器无法bind公网IP,对于服务器来讲,也不建议绑定确定的IP;
    INADDR_ANY宏默认为0,是让服务器在工作过程中,可以获取任意IP的数据;

    只要是发送到这个服务器的端口的数据都可以获取,不再指定IP地址;
    服务端只需要指定端口号;

cpp 复制代码
#ifndef _UDP_SERVER_HPP_
#define _UDP_SERVER_HPP_

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

#define SIZE 1024

class UdpServer
{
public:
    UdpServer(uint16_t port, std::string ip = "")
        : _port(port)
        , _ip(ip)
        , _sock(-1) // 套接字先初始化为-1
    {}

    bool initServer()
    {
        // 1.创建套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0); // AF_NET与PF_NET是一样的
        if(_sock < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        //2.bind:将用户设置的ip和port在内核中和我们当前的进程强关联
        struct sockaddr_in local; // 本地主机地址
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET; // 与套接字类型一致
        // 服务器的IP和端口号未来也是要发送给对方主机的,要先将数据发送到网络
        local.sin_port = htons(_port); // 考虑大小端转换
        // 将主机IP转换成网络IP
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        if(bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)// 绑定,须强转成同一类型struct sockaddr*
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(3);
        }
        logMessage(NORMAL, "init udp server done ... %s", strerror(errno));
        return true;
    }

    //直接显示客户端发的消息
    void start()
    {
        // 服务器是永不退出的
        char buffer[SIZE];
        for(;;)
        {
            struct sockaddr_in peer; // 远端地址,输出型参数
            bzero(&peer, sizeof(peer));
            socklen_t len = sizeof(peer);   // 输入输出型参数
                                            // 输入时,大小为src_addr的大小
                                            // 输出时,值为实际读到的dst_addr大小
            // 读取数据
            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
            if(s > 0)
            {
                buffer[s] = 0; //目前的数据当作字符串
                uint16_t cli_port = ntohs(peer.sin_port); //输出型参数,从网络中来的
                std::string cli_ip = inet_ntoa(peer.sin_addr); // 网络4字节IP转为主机IP
                printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);
            }
            //写回数据
            sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
        }
    }


    ~UdpServer()
    {
        if(_sock >= 0)
        {
            close(_sock); // 析构关闭套接字
        }
    }

private:
    //一个服务器,一般需要ip地址和port(16位整数)
    uint16_t _port;
    std::string _ip;
    int _sock; // 套接字
};

#endif

udp_client.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

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


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

    // client要不要bind??要,但是一般client不会显示的bind,程序员不会自己bind
    // client是一个客户端 -> 普通人下载安装启动使用的-> 如果程序员自己bind了->
    // client 一定bind了一个固定的ip和port,万一,其他的客户端提前占用了这个port呢??
    // client一般不需要显示的bind指定port,而是让OS自动随机选择(什么时候做的呢?)
    std::string message;
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);
    char buffer[1024];
    while(true)
    {
        std::cout << "请输入你的信息# ";
        std::getline(std::cin, message);
        if(message == "quit") break;
        //发送消息
        //当client首次发送消息给服务器的时候,OS会自动给client bind服务器的IP和port
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        //接受回信
        ssize_t s = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);
        if(s > 0)
        {
            buffer[s] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }   
    close(sock);

    return 0;
}
  • client不需要bind特定的套接字
    client是一个客户端 -> 普通人下载安装启动使用的-> 如果程序员自己bind了->一定bind了一个固定的ip和port,万一,其他的客户端提前占用了这个port呢;
    client一般不需要显示的bind指定port,而是让OS自动随机选择;
    当client首次发送消息给服务器的时候,OS会自动给client bind服务器的IP和port;

udp_server.cc

cpp 复制代码
#include"udp_server.hpp"
#include<memory>
#include<cstdlib>

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

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

    uint16_t port = atoi(argv[1]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port)); // 使用智能指针管理对象
    svr->initServer();
    svr->start();

    return 0;
}
  • 服务器不需要指定客户的IP地址获取数据;
    让服务器再工作过程中,可以获取任意IP的数据;
    只要是发送到这个服务器的端口的数据都可以获取,不再指定IP地址;
    服务端只需要指定端口号;

Log.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>

// 日志级别
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4

const char *gLevelMap[] = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"
};

#define LOGFILE "./threadpool.log"

// 完整的日志功能,至少需要:日志等级 时间 支持用户自定义(日志内容,文件行,文件名)

void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
    if (level == DEBUG)
        return;
#endif

    char stdBuffer[1024]; // 标准部分
    time_t timestamp = time(nullptr);
    snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%ld] ", gLevelMap[level], timestamp);

    char logBuffer[1024]; // 自定义部分
    va_list args;
    va_start(args, format);
    vsnprintf(logBuffer, sizeof(logBuffer), format, args);
    va_end(args);

    // FILE *fp = fopen(LOGFILE, "a");
    // fprintf(fp, "%s %s\n", stdBuffer, logBuffer);
    // fclose(fp);

    printf("%s %s\n", stdBuffer, logBuffer);
}

上面的代码实现了服务器直接打印客户端发送的信息,并将信息在发回客户端;

  • netstat -anup指令:查看当前网络中的UDP协议服务器状态

运行结果:

2.执行客户端发来的指令


*popen接口可以执行传入的字符串command;
执行command,创建pipe管道进行进程间通信,fork子进程执行(exec
)command命令;
FILE可以将执行成果通过FILE指针进行读取;**

udp_server.hpp中的start成员函数

cpp 复制代码
    // 执行客户端指令
    void start()
    {
        // 服务器是永不退出的
        char buffer[SIZE];
        for (;;)
        {
            struct sockaddr_in peer; // 远端地址,输出型参数
            bzero(&peer, sizeof(peer));
            socklen_t len = sizeof(peer); // 输入输出型参数
                                          // 输入时,大小为src_addr的大小
                                          // 输出时,值为实际读到的dst_addr大小
            char result[256];
            std::string cmd_echo;
            // 读取数据
            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                buffer[s] = 0; // 目前的数据当作字符串
                if (strcasestr(buffer, "rm") != nullptr || strcasestr(buffer, "rmdir"))
                {
                    std::string err_message = "bad !";
                    std::cout << err_message << buffer << std::endl;
                    sendto(_sock, err_message.c_str(), err_message.size(), 0, (struct sockaddr *)&peer, len);
                    continue;
                }
                FILE* fp = popen(buffer, "r");// 执行指令
                if(nullptr == fp)
                {
                    logMessage(ERROR, "popen: %d:%s", errno, strerror(errno));
                    continue;
                }
                while(fgets(result, sizeof(result), fp) != nullptr) //通过fp读取执行结果
                {
                    cmd_echo += result;
                }
                fclose(fp);
            }
            // 写回数据
            sendto(_sock, cmd_echo.c_str(), cmd_echo.size(), 0, (struct sockaddr *)&peer, len);
        }
    }

运行结果:

3.多用户聊天

服务器将返回的消息发送给所有用户,聊天室功能;
udp_server.hpp中的start函数

cpp 复制代码
class UdpServer
{
public:
    void start()
    {
        // 服务器是永不退出的
        char buffer[SIZE];
        for (;;)
        {
            struct sockaddr_in peer; // 远端地址,输出型参数
            bzero(&peer, sizeof(peer));
            socklen_t len = sizeof(peer); // 输入输出型参数
                                          // 输入时,大小为src_addr的大小
                                          // 输出时,值为实际读到的dst_addr大小
            char key[64];                 // 保存客户端地址信息
            // 读取数据
            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                buffer[s] = 0; // 目前的数据当作字符串
                uint16_t cli_port = ntohs(peer.sin_port); // 获取端口号
                std::string cli_ip = inet_ntoa(peer.sin_addr); // 获取4字节ip地址
                snprintf(key, sizeof key, "%s-%u", cli_ip.c_str(), cli_port); //格式化打印到key中
                logMessage(NORMAL, "key: %s", key);
                auto it = _users.find(key);
                if(it == _users.end()) // 若用户不存在,则添加用户
                {
                    logMessage(NORMAL, "add new user : %s", key);
                    _users.insert({key, peer});
                }
            }
            for(auto &iter : _users) // 将消息推送给所有用户
            {
                std::string sendMessage = key;
                sendMessage += "# ";
                sendMessage += buffer; // 发回消息的时候加上用户信息
                logMessage(NORMAL, "push message to %s", iter.first.c_str());
                sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr *)&(iter.second), sizeof(iter.second));
            }
        }
    }
    private:
        // 一个服务器,一般需要ip地址和port(16位整数)
        uint16_t _port;
        std::string _ip;
        int _sock; // 套接字
        std::unordered_map<std::string, struct sockaddr_in> _users; // 存储用户信息
    };

客户端一边收消息,一边发消息,多线程;
thread.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <cstdio>

typedef void *(*fun_t)(void *); // 定义函数指针类型,后面回调

class ThreadData // 线程信息结构体
{
public:
    void *_args;
    std::string _name;
};

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

        _tdata._args = args;
        _tdata._name = _name;
    }

    void start() // 创建线程
    {
        pthread_create(&_tid, nullptr, _func, (void *)&_tdata); // 直接将_tdata作为参数传给回调函数
    }

    void join() // 线程等待
    {
        pthread_join(_tid, nullptr);
    }

    std::string name()
    {
        return _name;
    }

    ~Thread()
    {
    }

private:
    std::string _name;
    fun_t _func;
    ThreadData _tdata;
    pthread_t _tid;
};

udp_client.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <memory>
#include "thread.hpp"

uint16_t server_port = 0;
std::string server_ip;

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

static void *udpSend(void *args)
{
    int sock = *(int *)((ThreadData *)args)->_args;
    std::string name = ((ThreadData *)args)->_name;
    std::string message;
    struct sockaddr_in server; // 服务器地址
    memset(&server, 0, sizeof server);
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());
    while (true)
    {
        std::cerr << "请输入你的信息# "; // 标准错误,fd == 2打印
        std::getline(std::cin, message);
        if (message == "quit")
        {
            break;
        }
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
    }
    return nullptr;
}

static void *udpRecv(void *args)
{
    int sock = *(int *)((ThreadData *)args)->_args;
    std::string name = ((ThreadData *)args)->_name;
    char buffer[1024];
    while(true)
    {
        memset(buffer, 0, sizeof buffer);
        struct sockaddr_in temp;
        socklen_t len = sizeof temp;
        ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr*)&temp, &len);
        if(s > 0)
        {
            buffer[s] = 0;
            std::cout << buffer << std::endl;
        }
    }
}

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

    server_ip = argv[1];
    server_port = atoi(argv[2]);

    // client要不要bind??要,但是一般client不会显示的bind,程序员不会自己bind
    // client是一个客户端 -> 普通人下载安装启动使用的-> 如果程序员自己bind了->
    // client 一定bind了一个固定的ip和port,万一,其他的客户端提前占用了这个port呢??
    // client一般不需要显示的bind指定port,而是让OS自动随机选择(什么时候做的呢?)
    std::unique_ptr<Thread> sender(new Thread(1, udpSend, (void *)&sock)); // 发送消息线程
    std::unique_ptr<Thread> recver(new Thread(2, udpRecv, (void *)&sock)); // 接收消息线程
    sender->start();
    recver->start();
    sender->join();
    recver->join();

    close(sock);

    return 0;
}

运行结果:
通过创建管道文件,让客户端发送信息和接收信息的会话分开:

4.在windows环境下运行客户端,与云服务器下的Linux服务端通信

windows下的套接字编程与Linux下的大致接口相同,只需要添加一些windows下独有的语句就可以:
udpClient.cpp

cpp 复制代码
#pragma warning(disable:4996) //禁用报错
#include <WinSock2.h> //win套接字头文件
#include <iostream>
#include <string>
using namespace std;
#pragma comment(lib,"ws2_32.lib") //固定用法,引入win下的套接字库
uint16_t serverport = 8080;
std::string serverip = "120.78.126.148"; //云服务器公网ip
int main()
{
    // windows 独有的
    WSADATA WSAData;
    WORD sockVersion = MAKEWORD(2, 2); //选中库
    if (WSAStartup(sockVersion, &WSAData) != 0)
        return 0;
    SOCKET clientSocket = socket(AF_INET, SOCK_DGRAM, 0);
    if (INVALID_SOCKET == clientSocket)
    {
        cout << "socket error!";
        return 0;
    }
    sockaddr_in dstAddr;
    dstAddr.sin_family = AF_INET;
    dstAddr.sin_port = htons(serverport);
    dstAddr.sin_addr.S_un.S_addr = inet_addr(serverip.c_str());
    char buffer[1024];
    while (true)
    {
        std::string message;
        std::cout << "请输入# ";
        std::getline(std::cin, message);
        sendto(clientSocket, message.c_str(), (int)message.size(), 0, (sockaddr*)&dstAddr, sizeof(dstAddr));
        struct sockaddr_in temp;
        int len = sizeof(temp);
        int s = recvfrom(clientSocket, buffer, sizeof buffer, 0, (sockaddr*)&temp, &len);
        if (s > 0)
        {
            buffer[s] = '\0';
            std::cout << "server echo# " << buffer << std::endl;
        }
    }
    // windows 独有
    closesocket(clientSocket);
    WSACleanup();
    return 0;
}

运行结果:

四、TCP套接字

1.打印客户端信息并发回

(1)tcp_server.hpp

  • 套接字类型

    由于TCP协议是面向字节流的协议,因此套接字的类型需选择SOCK_STREAM;

  • 面向连接的协议
    因为TCP协议是面向连接的,当我们正式通信的时候,需要先建立连接;

    将套接字状态设置为监听状态;
    backlog:全链接队列长度;
    成功返回0,失败返回-1,并设置errno;

  • netstat -antp指令:查看网络中的TCP协议服务器

  • 获取连接

    accept接口:获取与客户端的连接;
    addr:输出型参数,拿到客户端的ip和端口号;
    addlen:输入输出型参数,输入服务器addr大小,输出客户端addr大小;
    返回值:成功:返回套接字,这是真正进行IO服务的套接字;失败返回-1;
    传入的sockfd参数那个套接字只是获取底层的连接,是监听套接字;

  • 读取消息
    TCP流式套接字可以直接使用read和write接口(recvfrom专用于UDP数据报读取);

单进程循环处理
一次处理一个客户端,处理完了一个,才能处理下一个;

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"

// 打印服务
static void service(int sock, const std::string &cli_ip, const uint16_t &cli_port)
{
    // 读取消息:TCP流式套接字可以直接使用read和write接口(recvfrom专用于UDP数据报读取)

    char buffer[1024];
    while (true)
    {
        ssize_t s = read(sock, buffer, sizeof(buffer - 1));
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << cli_ip << ":" << cli_port << "# " << buffer << std::endl;
        }
        else if (s == 0) // 对端关闭连接
        {
            logMessage(NORMAL, "%s:%d shutdown, me too!", cli_ip.c_str(), cli_port);
            break;
        }
        else
        {
            logMessage(ERROR, "read sock error, %d:%s", errno, strerror(errno));
            break;
        }
        write(sock, buffer, strlen(buffer));
    }
}

class TcpServer
{
private:
    const static int gbacklog = 20;

public:
    TcpServer(uint16_t port, std::string ip = "")
        : _port(port), _ip(ip)
    {
    }

    void initServer()
    {
        // 1.创建套接字socket -- 进程和文件
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create socket success, listensock: %d", _listensock);

        // 2.bind -- 文件 + 网络
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        if (bind(_listensock, (struct sockaddr *)&local, sizeof local) < 0)
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }

        // 3.因为TCP是面向连接的,当我们正式通信的时候,需要先建立连接
        if (listen(_listensock, gbacklog) < 0)
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "init server success");
    }

    void start()
    {
        while (true)
        {
            // 4.获取连接
            struct sockaddr_in src;                                               // 输出型参数,获取对方主机地址
            socklen_t len = sizeof src;                                           // 输入输出型参数,对方主机地址的长度
            int servicesock = accept(_listensock, (struct sockaddr *)&src, &len); // 得到真正进行IO服务的套接字
            if (servicesock < 0)
            {
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                continue; // 获取连接失败后继续获取
            }
            // 获取连接成功了,通信对象的地址在accept函数的后两个参数中
            uint16_t cli_port = ntohs(src.sin_port);
            std::string cli_ip = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "link succsee, servicesock: %d | %s : %d |\n",
                       servicesock, cli_ip.c_str(), cli_port);
            // 开始进行通信服务
            // 单进程循环版 -- 一次处理一个客户端,处理完一个才能处理下一个
            service(servicesock, cli_ip, cli_port);
            close(servicesock);
        }
    }

    ~TcpServer()
    {
    }

private:
    uint16_t _port;
    std::string _ip;
    int _listensock; // 监听套接字
};

(2)tcp_server.cc

cpp 复制代码
#include "tcp_server.hpp"
#include <memory>
static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " port\n" << std::endl;
}
// ./tcp_server port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<TcpServer> svr(new TcpServer(port));
    svr->initServer();
    svr->start();
    return 0;
}
  • 使用远程登陆工具telnet模拟客户端

    连接服务端

    按照提示按下ctrl + ]进入telnet

运行结果

如果创建两个客户端

服务端只会响应第一个客户端,只有当第一个客户端退出时,第二个客户端才会被响应;

(3)多进程版服务器
tcp_server.hpp中的start成员函数

**父进程waitpid会阻塞进程,和单进程就没区别了;

  • 方案一:
    可以主动忽略SIGCHID信号让子进程自动释放僵尸状态;
    父进程关闭servicesock,因为每个进程可用的文件描述符都是有限的,这里关闭了,还有子进程的fd指向文件,因此不会有问题;
    如果不关闭,就会导致文件描述符泄露;
cpp 复制代码
    void start()
    {
        signal(SIGCHLD, SIG_IGN); // 对于SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
        while (true)
        {
            // 4.获取连接
            struct sockaddr_in src;                                               // 输出型参数,获取对方主机地址
            socklen_t len = sizeof src;                                           // 输入输出型参数,对方主机地址的长度
            int servicesock = accept(_listensock, (struct sockaddr *)&src, &len); // 得到真正进行IO服务的套接字
            if (servicesock < 0)
            {
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                continue; // 获取连接失败后继续获取
            }
            // 获取连接成功了,通信对象的地址在accept函数的后两个参数中
            uint16_t cli_port = ntohs(src.sin_port);
            std::string cli_ip = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "link succsee, servicesock: %d | %s : %d |\n",
                       servicesock, cli_ip.c_str(), cli_port);
            // 开始进行通信服务

            //多进程版 -- 创建子进程
            //让子进程给新的连接提供服务,子进程是能够直接打开父进程曾经打开的文件fd的
            pid_t id = fork();
            assert(id != -1);
            if(id == 0)
            {
                //子进程,能够继承父进程的文件fd
                //子进程是来提供服务的,不需要知道监听socket
                close(_listensock);//关闭不需要的文件描述符
                service(servicesock, cli_ip, cli_port);
                exit(0); // 使用忽略SIGCHLD信号来自动退出僵尸状态
            }
            close(servicesock);
        }
    }

运行结果:
可以进行多客户端通信了;

  • 方案二(不忽略SIGCHLD信号)
    在子进程中再fork,创建孙子进程,让孙子进程执行service,子进程立马退出;
    孙子进程就变成孤儿进程,让OS领养,OS在孤儿进程退出的时候,由OS自动回收孤儿进程;
cpp 复制代码
    void start()
    {
        while (true)
        {
            // 4.获取连接
            struct sockaddr_in src;                                               // 输出型参数,获取对方主机地址
            socklen_t len = sizeof src;                                           // 输入输出型参数,对方主机地址的长度
            int servicesock = accept(_listensock, (struct sockaddr *)&src, &len); // 得到真正进行IO服务的套接字
            if (servicesock < 0)
            {
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                continue; // 获取连接失败后继续获取
            }
            // 获取连接成功了,通信对象的地址在accept函数的后两个参数中
            uint16_t cli_port = ntohs(src.sin_port);
            std::string cli_ip = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "link succsee, servicesock: %d | %s : %d |\n",
                       servicesock, cli_ip.c_str(), cli_port);

            // 多进程版 -- 创建子进程
            // 让子进程给新的连接提供服务,子进程是能够直接打开父进程曾经打开的文件fd的
            pid_t id = fork();
            assert(id != -1);

            // 多进程版 -- 创建孙子进程
            if (id == 0)
            {
                // 子进程,能够继承父进程的文件fd
                // 子进程是来提供服务的,不需要知道监听socket
                close(_listensock); // 关闭不需要的文件描述符
                if(fork() > 0)
                {
                    exit(0);// 子进程调用fork,创建孙子进程,然后子进程退出
                }
                // 孙子进程在子进程退出后编程孤儿进程,OS领养,OS在孤儿进程退出后自动回收
                service(servicesock, cli_ip, cli_port);
                exit(0); // 使用忽略SIGCHLD信号来自动退出僵尸状态
            }
            waitpid(id, nullptr, 0); // 子进程及时退出,不会阻塞等待
            close(servicesock);
        }
    }

运行结果:

(4)客户端:
TCP的服务端需要bind,一定需要一个确定的port;
TCP的客户端不需要bind,一旦bind,就说明客户端绑定的是一个具体端口号,两个客户端可能是由不同公司写的,可能端口号会出现冲突;
需要让操作系统自动进行port选择;
客户端需要连接别人;

  • connect接口:连接特定的IP和port

    后两个参数与sendto参数相同,目的主机的地址和地址的大小;
    成功返回0,失败返回-1;

  • send接口:tcp发送接口

    flags默认为0

  • recv接口:tcp接收接口

注:send和recv也可以使用read和write代替;

tcp_client.cc

cpp 复制代码
#include "tcp_server.hpp"
#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>

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

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

    std::string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);
    bool alive = false; // 连接是否还存在
    int sock = 0;
    std::string line;
    while (true)
    {
        if (!alive) // 如果连接不存在,重新建立连接
        {
            sock = socket(AF_INET, SOCK_STREAM, 0);
            if (sock < 0)
            {
                std::cerr << "socket error" << std::endl;
                exit(2);
            }

            // client不需要显式bind,但是一定需要port
            // 要让OS自动进行port选择

            struct sockaddr_in server;
            memset(&server, 0, sizeof server);
            server.sin_family = AF_INET;
            server.sin_port = htons(serverport);
            server.sin_addr.s_addr = inet_addr(serverip.c_str());
            if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0)
            {
                std::cerr << "connect error" << std::endl;
                exit(3);
            }
            std::cout << "connect success" << std::endl;
            alive = true;
        }
        std::cout << "请输入# ";
        std::getline(std::cin, line);
        if(line == "quit")
        {
            break;
        }
        ssize_t s = send(sock, line.c_str(), line.size(), 0); // 向服务器发送消息
        if(s > 0)
        {
            // 发送成功
            char buffer[1024];
            ssize_t l = recv(sock, buffer, sizeof(buffer) - 1, 0);
            if(s > 0)
            {
                // 接收回信成功
                buffer[s] = 0;
                std::cout << "server 回显# " << buffer << std::endl;
            }
            else if(s == 0)
            {
                //接收到文件结尾
                alive = false;
                close(sock);
            }
        }
        else
        {
            // 发送失败,则重新开始建立连接
            alive = false;
            close(sock);
        }
    }

    return 0;
}

运行结果:

2.多线程版服务器

(1)每次连接时都创建新的线程
tcp_server.hpp

cpp 复制代码
class ThreadData
{
public:
    int _sock;
    std::string _ip;
    uint16_t _port;
};

class TcpServer
{
private:
    const static int gbacklog = 20;

    static void *threadRoutine(void *args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData *>(args);
        service(td->_sock, td->_ip, td->_port);
        delete td;

        return nullptr;
    }

public:
    TcpServer(uint16_t port, std::string ip = "")
        : _port(port), _ip(ip)
    {
    }

    void initServer()
    {
        // 1.创建套接字socket -- 进程和文件
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create socket success, listensock: %d", _listensock);

        // 2.bind -- 文件 + 网络
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        if (bind(_listensock, (struct sockaddr *)&local, sizeof local) < 0)
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }

        // 3.因为TCP是面向连接的,当我们正式通信的时候,需要先建立连接
        if (listen(_listensock, gbacklog) < 0)
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "init server success");
    }
   
    // 多线程版
    void start()
    {
        while (true)
        {
            // 4.获取连接
            struct sockaddr_in src;                                               // 输出型参数,获取对方主机地址
            socklen_t len = sizeof src;                                           // 输入输出型参数,对方主机地址的长度
            int servicesock = accept(_listensock, (struct sockaddr *)&src, &len); // 得到真正进行IO服务的套接字
            if (servicesock < 0)
            {
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                continue; // 获取连接失败后继续获取
            }
            // 获取连接成功了,通信对象的地址在accept函数的后两个参数中
            uint16_t cli_port = ntohs(src.sin_port);
            std::string cli_ip = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "link succsee, servicesock: %d | %s : %d |\n",
                       servicesock, cli_ip.c_str(), cli_port);
            // 多线程
            ThreadData* td = new ThreadData();
            td->_sock = servicesock;
            td->_ip = cli_ip;
            td->_port = cli_port;
            pthread_t tid;
            // 多线程不需要关闭文件描述符,因为多线程共享文件描述符
            pthread_create(&tid, nullptr, threadRoutine, td);
        }
    }

    ~TcpServer()
    {
    }

private:
    uint16_t _port;
    std::string _ip;
    int _listensock; // 监听套接字
};

运行结果:

(2)使用线程池管理线程

引入之前写的线程池,具体代码见Linux知识点 -- Linux多线程(四)

线程池代码中:
Task.hpp

更改了回调函数的类型;

cpp 复制代码
#pragma once

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

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

//与上面的写法是等价的
using func_t = std::function<void (int, const std::string&, const uint16_t&, const std::string&)>;

class Task
{

public:
    Task() {}
    Task(int sock, const std::string ip, uint16_t port, func_t func) 
        : _sock(sock)
        , _ip(ip)
        , _port(port)
        , _func(func)
    {}

    void operator()(const std::string &name)
    {
        _func(_sock, _ip, _port, name);
    }

public:
    int _sock;
    std::string _ip;
    uint16_t _port;
    func_t _func;
};

tcp_server.hpp

更改了service回调函数和类内成员函数start,每次将任务push进任务队列,等待线程池处理;

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <signal.h>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <pthread.h>
#include <ctype.h>
#include "thread-pool/log.hpp"
#include "thread-pool/threadPool.hpp"
#include "thread-pool/Task.hpp"

static void service(int sock, const std::string &cli_ip,
                    const uint16_t &cli_port, const std::string &thread_name)
{
    // 读取消息:TCP流式套接字可以直接使用read和write接口(recvfrom专用于UDP数据报读取)

    char buffer[1024];
    while (true)
    {
        ssize_t s = read(sock, buffer, sizeof(buffer - 1));
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << thread_name << "|" <<  cli_ip << ":" << cli_port << "# " << buffer << std::endl;
        }
        else if (s == 0) // 对端关闭连接
        {
            logMessage(NORMAL, "%s:%d shutdown, me too!", cli_ip.c_str(), cli_port);
            break;
        }
        else
        {
            logMessage(ERROR, "read sock error, %d:%s", errno, strerror(errno));
            break;
        }
        write(sock, buffer, strlen(buffer));
    }
    close(sock); // 线程在回调函数中关闭不用的文件描述符
}


class TcpServer
{
private:
    const static int gbacklog = 20;

public:
    TcpServer(uint16_t port, std::string ip = "0.0.0.0")
        : _listensock(-1)
        , _port(port)
        , _ip(ip)
        , _threadpool_ptr(ThreadPool<Task>::getThreadPool())
    {}

    void initServer()
    {
        // 1.创建套接字socket -- 进程和文件
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            logMessage(FATAL, "create socket error, %d:%s", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "create socket success, listensock: %d", _listensock);

        // 2.bind -- 文件 + 网络
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        if (bind(_listensock, (struct sockaddr *)&local, sizeof local) < 0)
        {
            logMessage(FATAL, "bind error, %d:%s", errno, strerror(errno));
            exit(3);
        }

        // 3.因为TCP是面向连接的,当我们正式通信的时候,需要先建立连接
        if (listen(_listensock, gbacklog) < 0)
        {
            logMessage(FATAL, "listen error, %d:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "init server success");
    }


    // 线程池
    void start()
    {
        _threadpool_ptr->run(); // 启动线程池
        while (true)
        {
            // 4.获取连接
            struct sockaddr_in src;                                               // 输出型参数,获取对方主机地址
            socklen_t len = sizeof src;                                           // 输入输出型参数,对方主机地址的长度
            int servicesock = accept(_listensock, (struct sockaddr *)&src, &len); // 得到真正进行IO服务的套接字
            if (servicesock < 0)
            {
                logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                continue; // 获取连接失败后继续获取
            }
            // 获取连接成功了,通信对象的地址在accept函数的后两个参数中
            uint16_t cli_port = ntohs(src.sin_port);
            std::string cli_ip = inet_ntoa(src.sin_addr);
            logMessage(NORMAL, "link succsee, servicesock: %d | %s : %d |\n",
                       servicesock, cli_ip.c_str(), cli_port);
            // 线程池版本
            Task t(servicesock, cli_ip, cli_port, service);
            _threadpool_ptr->pushTask(t);
        }
    }

    ~TcpServer()
    {
    }

private:
    uint16_t _port;
    std::string _ip;
    int _listensock; // 监听套接字
    std::unique_ptr<ThreadPool<Task>> _threadpool_ptr; // 线程池指针
};

其他代码都与之前的一致;

运行结果:

也可以传入其他的回调函数,完成不同的功能;

五、关于地址转换函数

1.字符串转in_addr的函数


inet_aton地址转换函数:


inet_pton地址转换函数

2.in_addr转字符串的函数

  • 关于inet_ntoa
    因为inet_ntoa把结果放到自己内部的一个静态存储区,这样第二次调用时的结果会覆盖掉上一次的结果,因此inet_ntoa是线程不安全的;
    在多线程环境下,推荐使用inet_ ntop,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题;

六、TCP协议通讯流程

  • 服务器初始化
    调用socket,创建文件描述符;
    调用bind,将当前的文件描述符和ip/port绑定在一起;如果这个端口已经被其他进程占用了,就会bind失败;
    调用listen,声明当前这个文件描述符作为一个服务器的文件描述符,为后面的accept做好准备;
    调用accecpt,并阻塞,等待客户端连接过来;

  • 建立连接的过程(三次握手)
    调用socket,创建文件描述符;
    调用connect,向服务器发起连接请求;
    connect会发出SYN段并阻塞等待服务器应答(第一次);
    服务器收到客户端的SYN,会应答一个SYN-ACK段表示"同意建立连接"(第二次);
    客户端收到SYN-ACK后会从connect(返回,同时应答一个ACK段(第三次);

  • 数据传输的过程
    建立连接后,TCP协议提供全双工的通信服务;所谓全双工的意思是,在同-条连接中,同一时刻,通信双方可以同时写数据;相对的概念叫做半双工,同一条连接在同一时刻,只能由一方来写数据;
    服务器从accept()返回后立刻调用read),读socket就像读管道一样,如果没有数据到达就阻塞等待;
    这时客户端调用write0发送请求给服务器,服务器收到后从read(返回,对客户端的请求进行处理,在此期
    间客户端调用read()阻塞等待服务器的应答;
    服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求;
    客户端收到后从read()返回,发送下一条请求如此循环下去;

  • 断开连接的过程(四次挥手)
    如果客户端没有更多的请求了,就调用close()关闭连接,客户端会向服务器发送FIN段(第一次);
    此时服务器收到FIN后,会回应一个ACK,同时read会返回0 (第二次);
    read返回之后,服务器就知道客户端关闭了连接,也调用close关闭连接,这个时候服务器会向客户端发送一个FIN(第三次);
    客户端收到FIN,再返回一个ACK给服务器(第四次);

相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒2 天前
TShark:基础知识
linux
AlfredZhao2 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334663 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪3 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux
网络研究院3 天前
2026年网络安全
网络·安全·法律·法规·趋势·发展