Linux——网络编程套接字

一、认识socket编程

网络通信的本质是什么呢?

我们需要明确一点,网络通信需要IP地址和端口号IP地址在标识网络中唯一的一台主机,端口号标识主机中唯一的一个进程,IP地址加端口号就能标识网络中唯一的一台主机上唯一的一个进程。 也就是说,通过IP地址和端口号,就可以找到整个网络中唯一的一个进程。所以网络通信的本质就是进程间通信

什么是socket编程?

网络通信的本质是进程间通信,是一种需要依靠IP地址和端口号的进程间通信,我们把这种基于IP地址和端口号的进程间通信叫做socket通信

socket的中文意思是插座 ,socket通信的模式类似于插板插座这样的模式,通信双方都必须要知道对方的IP地址和端口号就相当于把插板和插线接通,这样才能进行通信。而基于这种模式进行网络通信的编程就是socket编程。(Socket,通常也称为"套接字")

如何进行socket编程?

进行网络编程需要依靠网络层的网络传输协议,而网络传输协议是在操作系统内部实现的,用户不能直接访问操作系统内部的代码数据,这个时候,**操作系统就要提供系统调用接口供用户使用,从而进行网络编程。**这些接口就是socket编程的接口。

socket编程接口是传输层 供给应用层 的编程接口,位于TCP/IP四层模型中的应用层与传输层之间,是应用层与传输层之间的桥梁。而传输层常用的通信协议有UDP协议和TCP协议,于是socket编程便有了基于UDP协议的编程和基于TCP协议的编程

二、认识相关网络接口

1.socket套接字

socket 套接字是网络编程的基础,它就类似于网络通信的传递数据的管道,我们可以通过不同的参数来塑造这个管道。

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain,int type,int protocol);

参数:

domain:指定套接字使用的地址族/协议族,套接字使用的类型决定了数据传输的网络协议层,常见的地址族/协议层

{

AF_INET:IPv4 地址族,使用32位IP地址( A=Address, F=Family, INET=Internet)

AF_INET6:IPv6 地址族,使用128位IP地址

AF_UNIX/AF_LOCAL:本地进程间通信,使用文件系统路径作为地址

}

简单理解:选择本地通信/网络通信

type:指定套接字类型,决定传输层通信特性

{

SOCK_DGRAM:数据报套接字(基于UDP协议)

SOCK_STREAM:流式套接字(基于TCP协议)

}

protocol :指定具体传输层协议,我们通常使用默认,设置为0即可

返回值:

成功返回 套接字描述符sockfd(本质是打开文件的文件描述符),失败返回 -1

(Linux下一切皆文件,进程间通信可以理解为将数据从一个文件的缓冲区发送到另一个文件的缓冲区)

2.sockaddr_in网络地址结构体

我们会看到三种网络地址结构体:sockaddr、sockaddr_in、sockaddr_un

  • sockaddr_in用于网络通信,表示 IPv4 地址结构
  • sockaddr_un用于本地通信,表示unix域套接字地址
  • sockaddr用于统一接口,传参时sockaddr_in*和sockaddr_un*都转化为sockaddr*,在函数内部通过sockaddr结构体首地址的常数AF_INET/AF_UNIX来区分

重点讲解sockaddr_in结构体

cpp 复制代码
#include <netinet/in.h>
#include <arpa/inet.h>
struct sockaddr_in
{
    sa_family_t  sin_family;    // 地址族
    in_poet_t    sin_port;      // 16位端口号
    struct in_addr sin_addr;    // 32位 IPv4 地址的结构体
    unsigned char  sin_zero[8]; // 填充字段,常为0
};

typedef uint32_t in_addr_t;
struct in_addr
{
    in_addr_t s_addr;           // 专门存储 IPv4 地址
};

参数:

**sin_family:**地址类型,AF_INET 固定为 IPv4 地址结构

**sin_port:**通信端口号

**sin_addr:**存储IP地址的结构体

**s_addr:**将IP地址转化为网络字节序大端进行存储
补充:IP地址的两种存储模式

  • 点分十进制风格的IP("192.168.1.100"):字符串类型,可读性好
  • 整数风格的IP:uint32_t类型,网络通信使用

将点分十进制风格的IP转化为整数风格的IP:inet_addr

inet_addr内部完成了两件事:

  1. char*字符串类型转化为uint32_t类型
  2. 大小端转化:主机字节序转换为网络字节序(htonl()的功能)
cpp 复制代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

in_addr_t inet_addr(const char *cp);

参数:

cp:要转化的IP字符串
将整数风格的IP转化为点分十进制风格的IP:inet_ntoa/inet_ntop

将 32位网络字节序的 IPv4 地址转换为点分十进制字符串,inet_ntoa 仅支持 IPv4 在多线程中容易出问题,现代编程更加推荐 inet_ntop

inet_ntop内部完成了两件事:

  1. 大小端转化:网络字节序转换为主机字节序(ntohl()的功能)
  2. uint32_t类型转化为char*字符串类型
cpp 复制代码
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);
const char *inet_ntop(int af,const void *src,char *dst,socklen_t size);

参数

af:地址族( AF_INET/AF_INET6/......)

src:指向要转化的 IP 地址的指针

dst:指向数组的指针用于储存转换后的字符串

size:数组大小

3.bind绑定

bind 的作用是将套接字与特定的 IPport 进行绑定,我们已经将 IP 和 端口号 写入结构体 struct sockaddr_in 中,我们只需要传递结构体即可

cpp 复制代码
#include <sys/socket.h>
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);

参数:

**sockfd:**套接字描述符(本质是文件描述符)

**addr:**指向存储 IP 和 port 的结构体指针

**addrlen:**地址结构体的大小

返回值:

成功返回0,失败返回-1

4. recvfrom 接收网络数据

主要用于UDP协议接收网络数据报内容,同时获取发送方的地址信息

cpp 复制代码
#include <sys/socket.h>
ssize_t recvfrom(int sockfd,void *buf,size_t len,int flags,
                 struct sockaddr *src_addr,socklen_t *addrlen);

参数:

sockfd:套接字描述符(本质是文件描述符)

buf:接收数据的数组指针

len:数组的大小

flags:接收方式标志,通常为0,表示阻塞式读取(有数据就读,没数据就等)

src_addr:输出型参数,指向地址结构体,用于储存发送方的IP和port

addrlen:输入输出型参数,结构体大小,传入时表示传入的结构体大小,调用完成后表示收到的结构体大小

返回值:

成功返回接收到的字节数,失败返回-1

5. sendto 发送网络数据

主要用于UDP协议发送网络数据报内容,同时发送发送方的IP和port

cpp 复制代码
#include <sys/socket.h>
ssize_t sendto(int sockfd,const void *buf,size_t len,int flags,
               const struct sockaddr *dest_addr,socklen_t addrlen);

参数

sockfd:套接字描述符

buf:储存发送信息的数组指针

len:数组的大小

flags:发送方式标志,默认为0,阻塞发送(有数据就发,没数据就等)

dest_addr:指向结构体存储的接收方的IP和端口号

addrlen:结构体的大小

返回值:

成功返回实际字节数,失败返回-1

6.大小端转化

**在 TCP/IP 中统一采用大端字节序,即低地址高字节的方式。**为了保证输出主机和接收主机字节序一致,我们使用系统接口进行大小端转换。

ntohs / ntohl 网络字节序转换

ntohsntohl 都是将网络字节序转换为主机字节序ntohs 将 16 位网络字节序转换为 16 位的主机字节序(Network to Host Short),ntohl将 32 位网络字节序转换为 32 位的主机字节序(Network to Host Long)

cpp 复制代码
#include <arpa/inet.h>
uint16_t ntohs(uint16_t netshort);
uint32_t ntohl(uint32_t netlong);

参数

netshort / netlong:需要转换的端口号

htons / htonl 主机字节序端口号转换

htons 和 htonl 都是将主机字节序转换为网络字节序,htons 将 16 位主机字节序转换为 16 位的网络字节序(Host to Network Short),htonl 将 32 位主机字节序转换为 32 位的网络字节序(Host to Network Long)

cpp 复制代码
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort);
uint32_t htonl(uint32_t hostlong);

参数

hostshort / hostlong:需要转换的端口号

补充:

接收数据和发送数据时,由于使用了recvfrom和sendto系统调用接口,会自动进行传输数据的大小端转化,用户无需再手动转化

三、基于UDP的socket编程

服务器端程序编写步骤

核心步骤:创建套接字 → 绑定地址端口 → 循环接收并处理数据

1.创建套接字socket

cpp 复制代码
//1. 创建socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // IPv4协议  UDP模式  默认值
if (_sockfd == -1)
{
    cerr << "socket error" << strerror(errno) << endl;
    exit(2);
}

2.将本地信息和网络信息进行绑定

cpp 复制代码
//2. 绑定bind

// 填充服务器地址信息结构体
struct sockaddr_in local;           //  IPv4 网络地址结构体
bzero(&local, sizeof(local));       // 清空结构体
local.sin_family = AF_INET;         //表示使用 IPv4 协议
local.sin_port = htons(_port);      // 端口号 htons 主机字节序转网络字节序
//local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 将点分十进制的字符串 IP 转换为网络字节序的二进制形式
local.sin_addr.s_addr = INADDR_ANY; // 系统定义的宏(值为 0x00000000,对应 IPv4 地址 0.0.0.0)

//  这里为什么服务端要 bind !!!!
//  服务端需要稳定固定的地址,但客户端用临时地址即可
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
    cerr << "bind error" << strerror(errno) << endl;
    exit(3);
}

用于本地测试的IP地址

127.0.0.1是本地回环IP地址,用于本地回环通信,测试本机的网络配置

数据不会经过物理层

服务器绑定的IP地址说明

**一款网络服务器不建议绑定一个明确的IP地址。**有可能一个服务器可以通过多个IP地址进行访问,如果与一个IP地址进行显式绑定,那就无法接收通过其他IP地址传输的数据

举个例子,服务器可以从多个窗口中拿取数据,但如果与其中一个窗口显式绑定,就只能从该窗口中拿取数据,无法接收到其他窗口的数据

所以,在绑定服务端IP地址时,一般绑定"0.0.0.0"表示任意地址绑定。 这样一来,**数据通过一个IP地址发送到本机,只要是对应端口号,服务器都能接收,**与本机接收数据的IP地址无关

使用方法:

cpp 复制代码
/* Address to accept any incoming messages.  */
#define	INADDR_ANY		((in_addr_t) 0x00000000)

local.sin_addr.s_addr = INADDR_ANY; // 系统定义的宏(值为 0x00000000,对应 IPv4 地址 0.0.0.0)

所以,IP的缺省值也是"0.0.0.0"

3.接收数据

服务器端程序需要先收数据,再发消息进行响应。

cpp 复制代码
void Start()
{
    //3.接收数据
    while (true)
    {
        char buffer[1024];       // 接收客户端数据
        struct sockaddr_in peer; // IPv4 数据接收结构体
        socklen_t len = sizeof(peer);//必填
        ssize_t s = recvfrom(_sockfd, &buffer, sizeof(buffer) - 1, 0, (sockaddr *)&peer, &len); // 接收客户端信息,buffer数组最后一位要为0

        if (s > 0)
        {
                buffer[s] = 0; // 字符串结尾标志
            int peer_port = ntohs(peer.sin_port);      // 获取端口号
            string peer_ip = inet_ntoa(peer.sin_addr); // 获取IP(将IP转换为string类格式)
            string message = buffer;// 接收到的消息
            cout << "Get Message From [" << peer_ip << ":" << peer_port << "] : " << message << endl;
        }
    }
}

recvfrom函数的len参数

len参数时一个输出型参数,传入时表示传入的sockaddr_in结构体大小,调用完成后表示收到的sockaddr_in结构体大小

所以传入时必须有:

cpp 复制代码
 socklen_t len = sizeof(peer);//必填

4.发送数据

cpp 复制代码
//4.处理数据,发送数据
string result = _func(buffer); // 将信息执行函数
const char* buffer_send = result.c_str();
sendto(_sockfd, buffer_send, result.size(), 0, (sockaddr *)&peer, len); // 处理后的消息发回给客户端

客户端程序编写步骤

核心步骤:创建套接字 → 直接发送/接收数据(无需绑定,系统自动分配端口)

1.创建套接字socket

cpp 复制代码
//1. 创建socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0); // IPv4协议  UDP模式  默认值
if (_sockfd == -1)
{
    cerr << "socket error" << strerror(errno) << endl;
    exit(2);
}

2.客户端不需要明确绑定

客户端不需要明确绑定,操作系统自动绑定

服务端需要绑定,是因为服务端需要提供明确的端口(IP+PORT)供客户端连接,这个端口一般情况下是不轻易改变的,这样才能保证连接的稳定性

而客户端需要端口,但是只是用于标识唯一性,每次发送数据时,由操作系统自动随机绑定即可。如果一个客户端是绑定固定IP地址的,那如果该IP地址被占用后,客户端就无法连接

如果明确绑定了端口,操作系统就不会再自动绑定

3.发送数据

当客户端创建socket套接字之后,直接就可以进行数据的收发了;需要明确的是,客户端需要先发送数据,然后接收数据

cpp 复制代码
void Start()
{
    //3.发送数据
    struct sockaddr_in server;         //  IPv4 网络地址结构体
    bzero(&server, sizeof(server));    // 清空结构体
    server.sin_family = AF_INET;       //表示使用 IPv4 协议
    server.sin_port = htons(_server_port);              // 端口号 htons 主机字节序转网络字节序
    server.sin_addr.s_addr = inet_addr(_server_ip.c_str()); // 将点分十进制的字符串 IP 转换为网络字节序的二进制形式

    string message;
    while (true)
    {
        cout << "Please Enter# ";
        getline(cin, message);
        if (message == "exit")
        {
            break;
        }
        //  发送数据给服务器
        ssize_t s = sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
        if (s < 0)
        {
            cerr << "sendto error" << strerror(errno) << endl;
            continue;
        }
    }
}

4.接收数据

cpp 复制代码
// 4.接收服务器回发的数据
char buffer[1024];
ssize_t len = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, nullptr, nullptr);
if (len < 0)
{
    cerr << "recvfrom error" << strerror(errno) << endl;
    continue;
}
buffer[len] = 0;
cout << "Server Echo# " << buffer << endl;

Echo Server 实现

主要实现服务端接收客户端的信息,经过处理后再发送给客户端

udpServer.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <functional>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
using namespace std;

typedef function<string(const string &)> func_t;
static const string default_ip = "0.0.0.0";


class UdpServer
{
private:
    bool _isrunning;
    uint16_t _port; // 端口号
    string _ip;     // IP地址
    int _sockfd;    // socket描述符(文件描述符)
    func_t _func;

public:
    UdpServer(func_t func, const uint16_t &port, const string &ip = default_ip)
        : _isrunning(false),
          _port(port),
          _ip(ip),
          _func(func),
          _sockfd(-1)
    {
    }

    void InitServer()
    {
        //1. 创建socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // IPv4协议  UDP模式  默认值
        if (_sockfd == -1)
        {
            cerr << "socket error" << strerror(errno) << endl;
            exit(2);
        }
        cout << "Socket Create Success!" << endl;
        //2. 绑定bind

        // 填充服务器地址信息结构体
        struct sockaddr_in local;           //  IPv4 网络地址结构体
        bzero(&local, sizeof(local));       // 清空结构体
        local.sin_family = AF_INET;         //表示使用 IPv4 协议
        local.sin_port = htons(_port);      // 端口号 htons 主机字节序转网络字节序
        //local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 将点分十进制的字符串 IP 转换为网络字节序的二进制形式(等价)
        local.sin_addr.s_addr = INADDR_ANY; // 系统定义的宏(值为 0x00000000,对应 IPv4 地址 0.0.0.0)

        //  这里为什么服务端要 bind !!!!
        //  服务端需要稳定固定的地址,但客户端用临时地址即可
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            cerr << "bind error" << strerror(errno) << endl;
            exit(3);
        }
    }

void Start()
{
    //3.接收数据
    _isrunning = true;
    while (_isrunning)
    {
        char buffer[1024];       // 接收客户端数据
        struct sockaddr_in peer; // IPv4 数据接收结构体
        socklen_t len = sizeof(peer);//必填
        //  接收完客户端信息,再进行发送
        ssize_t s = recvfrom(_sockfd, &buffer, sizeof(buffer) - 1, 0, (sockaddr *)&peer, &len); // 接收客户端信息,buffer数组最后一位要0

        if (s > 0)
        {
            buffer[s] = 0; // 字符串结尾标志
            int peer_port = ntohs(peer.sin_port);      // 获取端口号
            string peer_ip = inet_ntoa(peer.sin_addr); // 获取IP(将IP转换为string类格式)
            string message = buffer;// 接收到的消息
            cout << "Get Message From [" << peer_ip << ":" << peer_port << "] : " << message << endl;

            //4.处理数据,发送数据
            string result = _func(buffer); // 将信息执行函数
            // cout << result << endl;
            const char* buffer_send = result.c_str();
            sendto(_sockfd, buffer_send, result.size(), 0, (sockaddr *)&peer, len); // 处理后的消息发回给客户端
        }
    }
}

    ~UdpServer()
    {
    }
};

udpServer.cpp

cpp 复制代码
#include <iostream>
#include <memory>
#include "udpServer.hpp"
using namespace std;
string fun_c(const string&kk)
{
    string a = "hello,";
    a += kk;
    return a;
}
 
// 输入  ./udpServer  port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cerr << "Usages:" << argv[0] << " port" << endl;
        return 1;
    }
    uint16_t _port = atoi(argv[1]);
    unique_ptr<UdpServer> udpserver (new UdpServer(fun_c, _port));
    udpserver->InitServer();
    udpserver->Start();
    return 0;
}

udpClient.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <functional>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
using namespace std;

class UdpClient
{
private:
    int _sockfd; // socket描述符(文件描述符)
    string _server_ip;
    uint16_t _server_port;

public:
    UdpClient(const string &server_ip, const uint16_t &server_port)
        : _server_ip(server_ip),
          _server_port(server_port),
          _sockfd(-1)
    {
    }
    void InitClient()
    {
        // 1. 创建socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // IPv4协议  UDP模式  默认值
        if (_sockfd == -1)
        {
            cerr << "socket error" << strerror(errno) << endl;
            exit(2);
        }
        cout << "Socket Create Success!" << endl;
        // 2. 绑定bind  客户端不需要绑定
    }
    void Start()
    {
        // 3.发送数据给服务器
        struct sockaddr_in server;                              //  IPv4 网络地址结构体
        bzero(&server, sizeof(server));                         // 清空结构体
        server.sin_family = AF_INET;                            // 表示使用 IPv4 协议
        server.sin_port = htons(_server_port);                  // 端口号 htons 主机字节序转网络字节序
        server.sin_addr.s_addr = inet_addr(_server_ip.c_str()); // 将点分十进制的字符串 IP 转换为网络字节序的二进制形式

        string message;
        while (true)
        {
            cout << "Please Enter# ";
            getline(cin, message);
            if (message == "exit")
            {
                break;
            }
            //  发送数据给服务器
            ssize_t s = sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
            if (s < 0)
            {
                cerr << "sendto error" << strerror(errno) << endl;
                continue;
            }

            // 4.接收服务器回发的数据
            char buffer[1024];
            ssize_t len = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, nullptr, nullptr);
            if (len < 0)
            {
                cerr << "recvfrom error" << strerror(errno) << endl;
                continue;
            }
            buffer[len] = 0;
            cout << "Server Echo# " << buffer << endl;
        }
    }
    ~UdpClient()
    {
        if (_sockfd != -1)
        {
            close(_sockfd);
        }
    }
};

udpClient.cpp

cpp 复制代码
#include <iostream>
#include <memory>
#include "udpClient.hpp"
using namespace std;

// 输入  ./udpClient  server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        cerr << "Usages:" << argv[0] << " server_ip server_port" << endl;
        return 1;
    }
    string server_ip = argv[1];
    uint16_t server_port = stoi(argv[2]);
    unique_ptr<UdpClient> udpclinet (new UdpClient(server_ip, server_port));
    udpclinet->InitClient();
    udpclinet->Start();
    return 0;
}   

运行结果

Chat Server 实现

主要实现多个客户端连接同一个服务端,服务端收到一个客户端的消息后,要将该消息发给所有客户端,相当于群聊

与之前的Echo Server相比,只是服务端收到数据后的处理方式不同

onlineUser.hpp

管理在线的用户

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include <cstdint>
#include <arpa/inet.h>

using namespace std;
class User
{
private:
    string _ip;
    uint16_t _port;

public:
    User(const string &ip, uint16_t port)
        : _ip(ip), _port(port) {}
    string getIp() const { return _ip; }
    uint16_t getPort() const { return _port; }
    ~User() {}
};

class OnlineUser
{
private:
    unordered_map<string, User> _onlineUsers; // key: "ip:port"
public:
    void addUser(const string &ip, uint16_t port)
    {
        string key = ip + ":" + to_string(port);
        _onlineUsers.emplace(key, User(ip, port));
    }
    void removeUser(const string &ip, uint16_t port)
    {
        string key = ip + ":" + to_string(port);
        _onlineUsers.erase(key);
    }
    bool isUserOnline(const string &ip, uint16_t port) const
    {
        string key = ip + ":" + to_string(port);
        return _onlineUsers.find(key) != _onlineUsers.end();
    }
    void broadcastMessage(const string &message, int sockfd) const
    {
        for (const auto &pair : _onlineUsers)
        {
            const User &user = pair.second;
            struct sockaddr_in clientaddr;
            bzero(&clientaddr, sizeof(clientaddr));
            clientaddr.sin_family = AF_INET;
            clientaddr.sin_port = htons(user.getPort());
            clientaddr.sin_addr.s_addr = inet_addr(user.getIp().c_str());
            sendto(sockfd, message.c_str(), message.size(), 0, (sockaddr *)&clientaddr, sizeof(clientaddr));
        }
    }
};

udpServer.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <functional>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
using namespace std;

typedef function<void(const string &message, int sockfd, const struct sockaddr_in &clientaddr)> func_t;
static const string default_ip = "0.0.0.0";

class UdpServer
{
private:
    bool _isrunning;
    uint16_t _port; // 端口号
    string _ip;     // IP地址
    int _sockfd;    // socket描述符(文件描述符)
    func_t _func;

public:
    UdpServer(func_t func, const uint16_t &port, const string &ip = default_ip)
        : _port(port),
          _ip(ip),
          _func(func),
          _sockfd(-1)
    {
    }

    void InitServer()
    {
        // 1. 创建socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // IPv4协议  UDP模式  默认值
        if (_sockfd == -1)
        {
            cerr << "socket error" << strerror(errno) << endl;
            exit(2);
        }
        cout << "Socket Create Success!" << endl;

        // 2. 绑定bind

        // 填充服务器地址信息结构体
        struct sockaddr_in local;           //  IPv4 网络地址结构体
        bzero(&local, sizeof(local));       // 清空结构体
        local.sin_family = AF_INET;         // 表示使用 IPv4 协议
        local.sin_port = htons(_port);      // 端口号 htons 主机字节序转网络字节序
        local.sin_addr.s_addr = INADDR_ANY; // 系统定义的宏(值为 0x00000000,对应 IPv4 地址 0.0.0.0)

        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            cerr << "bind error" << strerror(errno) << endl;
            exit(3);
        }
    }

    void Start()
    {
        // 3.接收数据
        while (true)
        {
            char buffer[1024];            // 接收客户端数据
            struct sockaddr_in peer;      // IPv4 数据接收结构体
            socklen_t len = sizeof(peer); // 必填
            //  接收完客户端信息,再进行发送
            ssize_t s = recvfrom(_sockfd, &buffer, sizeof(buffer) - 1, 0, (sockaddr *)&peer, &len); // 接收客户端信息,buffer数组最后一位要为0

            if (s > 0)
            {
                buffer[s] = 0;                             // 字符串结尾标志
                int peer_port = ntohs(peer.sin_port);      // 获取端口号
                string peer_ip = inet_ntoa(peer.sin_addr); // 获取IP(将IP转换为string类格式)
                string message = buffer;                   // 接收到的消息
                cout << "Get Message From [" << peer_ip << ":" << peer_port << "] : " << message << endl;

                // 4.处理数据,发送数据
                _func(buffer, _sockfd, peer); // 回调函数处理数据
            }
        }
    }

    ~UdpServer()
    {
    }
};

udpServer.cpp

cpp 复制代码
#include <iostream>
#include <memory>
#include "udpServer.hpp"
#include "onlineUser.hpp"
using namespace std;

OnlineUser onlineUsers;
void messageRoute(const string &message,int sockfd, const struct sockaddr_in &clientaddr)
{
    string client_ip = inet_ntoa(clientaddr.sin_addr);
    uint16_t client_port = ntohs(clientaddr.sin_port);
    if(message=="online")
    {
        if(!onlineUsers.isUserOnline(client_ip,client_port))
        {
            onlineUsers.addUser(client_ip,client_port);
            cout<<"User ["<<client_ip<<":"<<client_port<<"] is now online."<<endl;
        }
        else
        {
            cout<<"User ["<<client_ip<<":"<<client_port<<"] is already online."<<endl;
        }
        return;
    }
    else if(message=="offline")
    {
        if(onlineUsers.isUserOnline(client_ip,client_port))
        {
            onlineUsers.removeUser(client_ip,client_port);
            cout<<"User ["<<client_ip<<":"<<client_port<<"] is now offline."<<endl;
        }
        else
        {
            cout<<"User ["<<client_ip<<":"<<client_port<<"] was not online."<<endl;
        }
        return;
    }
    if(onlineUsers.isUserOnline(client_ip,client_port))
    {
       onlineUsers.broadcastMessage("From [" + client_ip + ":" + to_string(client_port) + "] : " + message, sockfd);
    }
    else
    {
       string notice="You are not online. Please send 'online' command first.";
       sendto(sockfd, notice.c_str(), notice.size(), 0, (sockaddr *)&clientaddr, sizeof(clientaddr));
    }
}

// 输入  ./udpServer  port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cerr << "Usages:" << argv[0] << " port" << endl;
        return 1;
    }
    uint16_t _port = atoi(argv[1]);
    unique_ptr<UdpServer> udpserver (new UdpServer(messageRoute, _port));
    udpserver->InitServer();
    udpserver->Start();
    return 0;
}

udpClient.hpp

客户端的主线程负责发送数据,子线程负责接收数据

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <functional>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>

using namespace std;

class UdpClient
{
private:
    int _sockfd; // socket描述符(文件描述符)
    string _server_ip;
    uint16_t _server_port;
    pthread_t _recvThread;

public:
    UdpClient(const string &server_ip, const uint16_t &server_port)
        : _server_ip(server_ip),
          _server_port(server_port),
          _sockfd(-1)
    {
    }
    void InitClient()
    {
        // 1. 创建socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // IPv4协议  UDP模式  默认值
        if (_sockfd == -1)
        {
            cerr << "socket error" << strerror(errno) << endl;
            exit(2);
        }
        cout << "Socket Create Success!" << endl;
        // 2. 绑定bind  客户端不需要绑定
    }

    static void *recvThreadFunc(void *arg)
    {
        int sockfd = *((int *)arg);
        char buffer[1024];
        while (true)
        {
            ssize_t len = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, nullptr, nullptr);
            if (len < 0)
            {
                cerr << "recvfrom error" << strerror(errno) << endl;
                continue;
            }
            buffer[len] = 0;
            cout << buffer << endl;
            cout.flush();
        }
        return nullptr;
    }

    void Start()
    {
        pthread_create(&_recvThread, nullptr, recvThreadFunc, (void *)&_sockfd);
        pthread_detach(_recvThread);

        // 主线程发送数据给服务器

        struct sockaddr_in server;                              //  IPv4 网络地址结构体
        bzero(&server, sizeof(server));                         // 清空结构体
        server.sin_family = AF_INET;                            // 表示使用 IPv4 协议
        server.sin_port = htons(_server_port);                  // 端口号 htons 主机字节序转网络字节序
        server.sin_addr.s_addr = inet_addr(_server_ip.c_str()); // 将点分十进制的字符串 IP 转换为网络字节序的二进制形式

        string message;
        while (true)
        {
            cerr << "Please Enter# ";
            getline(cin, message);
            if (message == "exit")
            {
                break;
            }
            ssize_t s = sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
            if (s < 0)
            {
                cerr << "sendto error" << strerror(errno) << endl;
                continue;
            }

            // 子线程接收服务器回发的数据
        }
    }
    ~UdpClient()
    {
        if (_sockfd != -1)
        {
            close(_sockfd);
        }
    }
};

udpClient.cpp

cpp 复制代码
#include <iostream>
#include <memory>
#include "udpClient.hpp"
using namespace std;

// 输入  ./udpClient  server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        cerr << "Usages:" << argv[0] << " server_ip server_port" << endl;
        return 1;
    }
    string server_ip = argv[1];
    uint16_t server_port = stoi(argv[2]);
    unique_ptr<UdpClient> udpclinet (new UdpClient(server_ip, server_port));
    udpclinet->InitClient();
    udpclinet->Start();
    return 0;
}   

运行结果

为了让客户端输入界面和消息收取界面分开,创建管道,一个界面只负责发送数据,从服务器收到的数据重定向到管道,在另一个界面从管道中读取数据

登录后发送的消息才能被其他客户端看到

四、基于TCP的socket编程

服务端使用的新增网络接口

1.listen设置监听状态

cpp 复制代码
#include <sys/socket.h>
int listen(int sockfd, int backlog);

参数:

  • sockfd:是socket函数返回的文件描述符,代表了一个打开的套接字。
  • backlog:指定了系统应该为相应套接字排队的最大连接个数。如果队列满了,则新的连接请求将被拒绝。这个参数的值至少为0,但具体能支持的最大值依赖于系统实现。

返回值:

  • 成功时,返回0;出错时,返回-1,相应的错误码errno被设置。

2.accept获取连接

cpp 复制代码
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数:

  • sockfd:服务器端套接字描述符,这是由socket函数创建并已经通过bind函数绑定到特定地址的监听套接字。
  • addr:一个指向sockaddr结构体的指针,用于存储客户端的地址信息。如果调用者对此不关心,可以将此参数设置为NULL。
  • addrlen:一个指向socklen_t类型的指针,用于指定addr缓冲区的大小。在调用accept之前,应该将addrlen初始化为addr缓冲区的大小,函数返回时,addrlen会被设置为实际地址结构的长度。

返回值:

  • 成功时,accept函数返回一个新的套接字描述符(文件描述符),这个新的套接字描述符用于与客户端进行通信。
  • 出错时,返回-1,错误码被设置。
    成功时的返回值说明

将socket返回的套接字listen_sockfd传入listen函数后,listen_sockfd成为监听套接字,它的作用仅仅是获取新的连接。

当有连接建立时,accept函数就会从监听套接字中获取连接,并返回一个新的套接字(文件描述符),用于建立该连接的客户端和服务器之间的通信。

原因:服务器只有一个,但是客户端可以有很多,多个客户端和一个服务器之间要进行通信,就要求服务器中需要为每个连接建立用于通信套接字。

3.read读取数据

cpp 复制代码
#include <unistd.h>
ssize_t read(int fd, void buf[.count], size_t count);

参数:

  • fd:文件描述符(file descriptor),是一个非负整数,代表了一个打开的文件、管道或套接字。它是通过之前对 open、pipe、socket 等系统调用的调用获得的。
  • buf:指向缓冲区的指针,该缓冲区用于存储从文件描述符指向的资源中读取的数据。
  • count:请求读取的字节数。这是尝试从文件中读取的最大字节数。

返回值:

  • 成功:返回实际读取的字节数,这个值可能小于请求读取的字节数(count),尤其是在到达文件末尾(EOF)或发生某些类型的非阻塞 I/O 操作时。
  • 失败:返回 -1,并设置 errno 以指示错误原因。

4.write回写数据

cpp 复制代码
#include <unistd.h>
ssize_t write(int fd, const void buf[.count], size_t count);

参数:

  • fd:文件描述符(file descriptor),是一个非负整数,代表了一个打开的文件、管道或套接字。
  • buf:指向缓冲区的指针,该缓冲区包含了要写入文件描述符指向的资源中的数据。
  • count:要写入的字节数。

返回值:

  • 成功:返回实际写入的字节数。这个值可能与请求写入的字节数(count)相同,也可能不同,特别是在写入非阻塞文件描述符或管道时,如果没有足够的空间来保存所有请求的数据,则可能只写入部分数据。
  • 失败:返回 -1,并设置 errno 以指示错误原因。

客户端使用的新增网络接口

1.connect发起连接

cpp 复制代码
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数:

  • sockfd:这是由 socket 函数返回的文件描述符,代表一个套接字。
  • addr:这是一个指向 sockaddr 结构的指针,该结构包含了服务器端的地址和端口号信息。
  • addrlen:这个参数指定了 addr 参数所指向的地址结构体的长度。

返回值:

  • 成功时,返回0。
  • 出错时,返回-1,并设置全局变量 errno 以指示错误类型。

补充:

连接成功后,系统会自动绑定客户端

2.send发送数据(可用write代替)

cpp 复制代码
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数:

  • sockfd:socket函数创建的文件描述符。表明向哪个文件描述符中写入数据。
  • buf:指向包含要发送数据的缓冲区的指针。
  • len:要发送的字节数。
  • flags:调用标志,通常设置为0

返回值:

  • 成功时,返回实际发送的字节数。这个值可能小于请求发送的字节数,因为TCP是一个流式协议,不保证数据包的原子性。
  • 出错时,返回-1,并设置errno以指示错误原因。

3.recv接收数据(可用read代替)

cpp 复制代码
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数:

  • sockfd:套接字文件描述符,是之前通过 socket 函数创建,表明从哪个文件中读取数据。
  • buf:指向接收数据的缓冲区的指针。
  • len:缓冲区的大小,即可以接收的最大字节数。
  • flags:一组标志,用来修改 recv 函数的行为。最常用的标志是 0,表示正常接收数据。

返回值:

  • 成功:返回实际接收到的字节数。如果连接正常关闭,且没有数据可读,则返回 0。
  • 失败:返回 -1,并设置 errno 以指示错误原因。

服务端程序编写步骤

核心步骤 :创建套接字 → 绑定地址端口 → 监听连接接受连接 → 循环读写数据

1.创建套接字socket

socket()第二个参数改为 SOCK_STREAM,表明创建流式套接字,这是因为TCP是面向字节流的协议

cpp 复制代码
// 1. 创建socket
_listen_sockfd = socket(AF_INET, SOCK_STREAM, 0); // IPv4协议  TCP模式  默认值
if (_listen_sockfd == -1)
{
    cerr << "socket error" << strerror(errno) << endl;
    exit(2);
}
cout << "Socket Create Success!" << endl;

2. 绑定bind

使用上和udp服务器端的bind完全相同

cpp 复制代码
// 2. 绑定bind
struct sockaddr_in local;           //  IPv4 网络地址结构体
bzero(&local, sizeof(local));       // 清空结构体
local.sin_family = AF_INET;         // 表示使用 IPv4 协议
local.sin_port = htons(_port);      // 端口号 htons 主机字节序转网络字节序
local.sin_addr.s_addr = INADDR_ANY; // 系统定义的宏(值为 0x00000000,对应 IPv4 地址 0.0.0.0)

int n = bind(_listen_sockfd, (struct sockaddr *)&local, sizeof(local)); // 绑定socket与地址
if (n < 0)
{
    cerr << "bind error" << strerror(errno) << endl;
    exit(3);
}
cout << "Bind Success!" << endl;

3. 将流式套接字设置为监听状态

TCP是有连接的协议,客户端和服务器端需要建立连接 才能进行通信,将流式套接字设置为监听状态的目的是用于监听连接的状态

cpp 复制代码
// 3. 监听listen
int n = listen(_listen_sockfd, 5); // 最大连接数5
if (n < 0)
{
    cerr << "listen error" << strerror(errno) << endl;
    exit(4);
}
cout << "Listening On Port " << _port << " ..." << endl;

4.获取连接

cpp 复制代码
// 4. 接收accept  阻塞等待客户端连接
struct sockaddr_in peer;      // IPv4 数据接收结构体
socklen_t len = sizeof(peer); // 必填
int client_sockfd = accept(_listen_sockfd, (struct sockaddr *)&peer, &len);
if (client_sockfd < 0)
{
    cerr << "accept error" << strerror(errno) << endl;
    continue;
}
cout << "Get A New Client!, client_sockfd:" << client_sockfd << endl;

5.接收消息

因为TCP是面向字节流的协议,所以我们可以使用文件读写接口 readwrite

cpp 复制代码
// 5. 接收数据read
char buffer[1024];
ssize_t s = read(client_sockfd, &buffer, sizeof(buffer) - 1);

6.发送消息

cpp 复制代码
// 6. 回发数据write
string echo_message = "Server Echo: " + message;
ssize_t n = write(client_sockfd, echo_message.c_str(), echo_message.size());
if (n < 0)
{
    cerr << "write error" << strerror(errno) << endl;
    continue;
}

客户端程序编写步骤

核心步骤:创建套接字 → 连接服务器 → 循环读写数据

1.创建套接字socket

使用socket函数时,第二个参数设置SOCK_STREAM,表明是基于TCP协议进行的通信

cpp 复制代码
// 1. 创建socket
_sockfd = socket(AF_INET, SOCK_STREAM, 0); // IPv4协议  TCP模式  默认值
if (_sockfd == -1)
{
    cerr << "socket error" << strerror(errno) << endl;
    exit(2);
}
cout << "Socket Create Success!" << endl;

2.发起连接connect

TCP是有连接的协议,客户端想要和服务器端进行通信时,首先要发起建立连接的请求,服务器端的监听套接字监听到请求建立连接成功之后,accept函数就会从监听套接字中获取连接,并分配一个文件描述符,该文件描述符所关联的文件用于建立连接的双方进行通信。

这也就解释了服务器端为什么要设置监听套接字和获取连接的行为。

cpp 复制代码
// 2. 连接connect
struct sockaddr_in server;                              //  IPv4 网络地址结构体
bzero(&server, sizeof(server));                         // 清空结构体
server.sin_family = AF_INET;                            // 表示使用 IPv4 协议
server.sin_port = htons(_server_port);                  // 端口号 htons 主机字节序转网络字节序
server.sin_addr.s_addr = inet_addr(_server_ip.c_str()); // 将点分十进制的字符串 IP 转换为网络字节序的二进制形式

int n = connect(_sockfd, (struct sockaddr *)&server, sizeof(server));
if (n < 0)
{
    cerr << "connect error" << strerror(errno) << endl;
    exit(3);
}
cout << "Connect To Server Success!" << endl;

3.发送数据

cpp 复制代码
//3. 主线程发送数据给服务器
string message;
getline(cin, message);
ssize_t s = write(_sockfd, message.c_str(), message.size());
//或者
ssize_t s = send(_sockfd, message.c_str(), message.size(), 0);

4.接收数据

cpp 复制代码
// 4.子线程接收服务器回发的数据
char buffer[1024];
ssize_t n = read(_sockfd, &buffer, sizeof(buffer) - 1);
//或者
ssize_t n = recv(_sockfd, buffer, sizeof(buffer) - 1, 0);

Echo Server 实现

前备条件

log.hpp

制作简易的日志,用于后续打印日志报告

cpp 复制代码
#pragma once
#include <iostream>

#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4
void logMessage(int level, const std::string &msg)
{
    const char *levelStr = nullptr;
    switch (level)
    {
    case DEBUG:
        levelStr = "DEBUG";
        break;
    case NORMAL:
        levelStr = "NORMAL";
        break;
    case WARNING:
        levelStr = "WARNING";
        break;
    case ERROR:
        levelStr = "ERROR";
        break;
    case FATAL:
        levelStr = "FATAL";
        break;
    default:
        levelStr = "UNKNOWN";
        break;
    }
    std::cout << "[" << levelStr << "] " << msg << std::endl;
}

服务端

单进程版

由于服务端每次只能与一个客户端连接,所以同一时间只能处理一个客户端的数据 。当与唯一客户端断开连接后,才能与另一客户端建立连接,处理数据。这并不符合真实的服务器

cpp 复制代码
serviceClient(client_sockfd, peer);// 处理客户端请求
close(client_sockfd); // 关闭与客户端的连接
多进程版

通过创建子进程处理数据。 而主进程一般需要回收子进程,不然子进程完成任务后会变成僵尸进程,占用系统资源不释放。所以,下面是回收子进程资源的方法

(子进程继承父进程的文件描述符表,父子各一张)

法一:主进程阻塞等待子进程

同一时间只能处理一个客户端的数据

cpp 复制代码
pid_t pid = fork();
if (pid == 0)// 子进程
{
    close(_listen_sockfd); // 关闭监听的socket描述符
    serviceClient(client_sockfd, peer);
    close(client_sockfd); 
    exit(0);
}
// 父进程
waitpid(pid, NULL, 0); // 回收子进程资源
close(client_sockfd); // 关闭已连接的客户端socket描述符

法二:孙子进程

子进程创建后,创建孙子进程处理数据,子进程退出。由于子进程退出,主进程能够立刻回收子进程,继续创建新的孙子进程,孙子进程被系统领养,无需再管。

同一时间就可以处理多个客户端的数据

cpp 复制代码
pid_t pid = fork();
if (pid == 0)// 子进程
{
    if(fork() > 0)exit(0); // 让子进程成为孤儿进程,防止僵尸进程产生
    //孙子进程
    close(_listen_sockfd); 
    serviceClient(client_sockfd, peer);
    close(client_sockfd); 
    exit(0);
}
// 父进程
waitpid(pid, NULL, 0); // 回收子进程资源
close(client_sockfd); // 关闭已连接的客户端socket描述符

法三:自动回收

设置子进程退出时的信号捕捉动作为SIG_IGN,系统会自动回收进程,无需等待

同一时间就可以处理多个客户端的数据

cpp 复制代码
signal(SIGCHLD, SIG_IGN); // 防止僵尸进程产生
pid_t pid = fork();
if (pid == 0)// 子进程
{
    close(_listen_sockfd); 
    serviceClient(client_sockfd, peer);
    close(client_sockfd); 
    exit(0);
}
// 父进程
close(client_sockfd); // 关闭已连接的客户端socket描述符
多线程版

每当有一个新的客户端连接进来,就创建一个线程专门为该客户端服务,再将线程分离

同一时间就可以处理多个客户端的数据,无客户端连接数上限(来一个创建一个线程)

cpp 复制代码
std::thread t(&TcpServer::serviceClient, this, client_sockfd, peer);
t.detach(); // 分离线程
tcpServer.hpp
cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <functional>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <thread>
#include "log.hpp"
using namespace std;

enum
{
    USAGE_ERROR = 1,
    SOCKET_ERROR = 2,
    BIND_ERROR = 3,
    LISTEN_ERROR = 4,
    ACCEPT_ERROR = 5,
    READ_ERROR = 6,
    WRITE_ERROR = 7
};
class TcpServer
{
private:
    uint16_t _port;     // 端口号
    int _listen_sockfd; // socket描述符(文件描述符)

public:
    TcpServer(const uint16_t &port)
        : _port(port),
          _listen_sockfd(-1)
    {
    }

    void InitServer()
    {

        // 1. 创建socket
        _listen_sockfd = socket(AF_INET, SOCK_STREAM, 0); // IPv4协议  TCP模式  默认值
        if (_listen_sockfd == -1)
        {
            logMessage(FATAL, "socket error");
            exit(SOCKET_ERROR);
        }
        logMessage(NORMAL, "Socket Create Success!");

        // 2. 绑定bind
        struct sockaddr_in local;           //  IPv4 网络地址结构体
        bzero(&local, sizeof(local));       // 清空结构体
        local.sin_family = AF_INET;         // 表示使用 IPv4 协议
        local.sin_port = htons(_port);      // 端口号 htons 主机字节序转网络字节序
        local.sin_addr.s_addr = INADDR_ANY; // 系统定义的宏(值为 0x00000000,对应 IPv4 地址 0.0.0.0)

        int n = bind(_listen_sockfd, (struct sockaddr *)&local, sizeof(local)); // 绑定socket与地址
        if (n < 0)
        {
            logMessage(FATAL, "bind error");
            exit(BIND_ERROR);
        }
        logMessage(NORMAL, "Bind Success!");

        // 3. 监听listen
        n = listen(_listen_sockfd, 5); // 最大连接数5
        if (n < 0)
        {
            logMessage(FATAL, "listen error");
            exit(LISTEN_ERROR);
        }
        logMessage(NORMAL, "Listening On Port " + to_string(_port) + " ...");
    }

    void serviceClient(int client_sockfd, struct sockaddr_in peer)
    {
        while (true)
        {
            // 5. 接收数据read
            char buffer[1024];
            ssize_t s = read(client_sockfd, buffer, sizeof(buffer) - 1);

            if (s > 0)
            {
                buffer[s] = 0;                             // 字符串结尾标志
                int peer_port = ntohs(peer.sin_port);      // 获取端口号
                string peer_ip = inet_ntoa(peer.sin_addr); // 获取IP(将IP转换为string类格式)
                string message = buffer;                   // 接收到的消息
                cout << "Get Message From [" << peer_ip << ":" << peer_port << "] : " << message << endl;

                // 6. 回发数据write
                string echo_message = "[Server Echo] " + message;
                ssize_t n = write(client_sockfd, echo_message.c_str(), echo_message.size());
                if (n < 0)
                {
                    logMessage(ERROR, "write error" + string(strerror(errno)));
                    continue;
                }
            }
            else if (s == 0)
            {
                logMessage(NORMAL, "Client Quit!, client_sockfd:" + to_string(client_sockfd));
                break;
            }
            else
            {
                logMessage(ERROR, "read error" + string(strerror(errno)));
                break;
            }
        }
        close(client_sockfd);
    }

    void Start()
    {
        while (true)
        {

            // 4. 与客户端连接accept
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int client_sockfd = accept(_listen_sockfd, (struct sockaddr *)&peer, &len); //
            if (client_sockfd < 0)
            {
                logMessage(ERROR, "accept error" + string(strerror(errno)));
                continue;
            }
            logMessage(NORMAL, "Get A New Client!, client_sockfd:" + to_string(client_sockfd));

            std::thread t(&TcpServer::serviceClient, this, client_sockfd, peer);
            t.detach(); // 分离线程
        }
    }

    ~TcpServer()
    {
        if (_listen_sockfd != -1)
        {
            close(_listen_sockfd);
        }
    }
};
tcpServer.cpp
cpp 复制代码
#include <iostream>
#include <memory>
#include "tcpServer.hpp"
using namespace std;


// 输入  ./tcpServer  port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cerr << "Usages:" << argv[0] << " port" << endl;
        exit(USAGE_ERROR);
    }
    uint16_t _port = atoi(argv[1]);
    unique_ptr<TcpServer> tcpserver (new TcpServer(_port));
    tcpserver->InitServer();
    tcpserver->Start();
    return 0;
}
线程池版

提前创建好多个线程,有任务时,直接推送给线程运行

同一时间就可以处理多个客户端的数据,有客户端连接数上限(线程的数量有限)

Task.hpp
cpp 复制代码
#pragma once                                                                                                                                                                                                                                                                                                                                                                
#include <iostream>    
#include <pthread.h>   
#include <string> 
using namespace std;    
    

 void serviceClient(int client_sockfd, struct sockaddr_in peer)
    {
        while (true)
        {
            // 5. 接收数据read
            char buffer[1024];
            ssize_t s = read(client_sockfd, buffer, sizeof(buffer) - 1);

            if (s > 0)
            {
                buffer[s] = 0;                             // 字符串结尾标志
                int peer_port = ntohs(peer.sin_port);      // 获取端口号
                string peer_ip = inet_ntoa(peer.sin_addr); // 获取IP(将IP转换为string类格式)
                string message = buffer;                   // 接收到的消息
                cout << "Get Message From [" << peer_ip << ":" << peer_port << "] : " << message << endl;

                // 6. 回发数据write
                string echo_message = "[Server Echo] " + message;
                ssize_t n = write(client_sockfd, echo_message.c_str(), echo_message.size());
                if (n < 0)
                {
                    logMessage(ERROR, "write error" + string(strerror(errno)));
                    continue;
                }
            }
            else if (s == 0)
            {
                logMessage(NORMAL, "Client Quit!, client_sockfd:" + to_string(client_sockfd));
                break;
            }
            else
            {
                logMessage(ERROR, "read error" + string(strerror(errno)));
                break;
            }
        }
        close(client_sockfd);
    }

    class Task    
    {    
    private:    
        int _child_sockfd;
        struct sockaddr_in _peer;
    public:    
        Task():_child_sockfd(-1){}    
        Task(int child_sockfd, struct sockaddr_in peer):_child_sockfd(child_sockfd), _peer(peer){}

    
        void Run()    
        {    
            serviceClient(_child_sockfd, _peer);
        }
        void operator()()    
        {    
            Run();
        }    
    
        ~Task(){}    
    };    
thread_pool.hpp
cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <queue>
#include <unistd.h>
#include <pthread.h>

using namespace std;

namespace ns_threadpool
{
    const int g_num = 3;
    template <class T>
    class ThreadPool
    {
    private:
        int num_;
        vector<pthread_t> threads_;
        queue<T> task_queue_;
        pthread_mutex_t mtx_;
        pthread_cond_t cond_;
        static ThreadPool<T> *ins; // 类内的静态指针

    private:
        // 构造函数私有化
        ThreadPool(int num = g_num) : num_(num)
        {
            pthread_mutex_init(&mtx_, nullptr);
            pthread_cond_init(&cond_, nullptr);
        }
        ThreadPool(const ThreadPool<T> &) = delete;               // 禁止拷贝构造函数
        ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // 禁止赋值操作符重载
    public:
        static ThreadPool<T> *GetInstance() // 单例模式获取线程池对象
        {
            static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
            if (ins == nullptr) // 先判断,不用直接申请锁,提升效率
            {
                pthread_mutex_lock(&lock);
                if (ins == nullptr)
                {
                    ins = new ThreadPool<T>();
                    ins->InitThreadPool();
                    cout << "线程池单例对象创建成功!" << endl;
                }
                pthread_mutex_unlock(&lock);
            }
            return ins;
        }
        void Lock() { pthread_mutex_lock(&mtx_); }

        void Unlock() { pthread_mutex_unlock(&mtx_); }

        bool IsEmpety() { return task_queue_.empty(); }

        void Wait() { pthread_cond_wait(&cond_, &mtx_); }

        void WakeUp() { pthread_cond_signal(&cond_); }

    public:
        ~ThreadPool()
        {
            pthread_mutex_destroy(&mtx_);
            pthread_cond_destroy(&cond_);
        }

        // 在类中要让线程执行类内成员方法,是不可行的
        // 必须让线程执行静态方法
        static void *Rountine(void *args)
        {
            pthread_detach(pthread_self());
            ThreadPool<T> *tp = (ThreadPool<T> *)args;
            while (true)
            {
                tp->Lock();
                while (tp->IsEmpety())
                {
                    tp->Wait();
                }
                T t;
                tp->PopTask(&t);
                tp->Unlock();
                t.Run();
            }
        }

        void InitThreadPool()
        {
            pthread_t tid;
            for (int i = 0; i < num_; i++)
            {
                pthread_create(&tid, nullptr, Rountine, (void *)this /*传this指针给静态方法*/);
                threads_.push_back(tid);
            }
        }

        void PushTask(const T &in)
        {
            Lock();
            task_queue_.push(in);
            Unlock();
            WakeUp();
        }

        void PopTask(T *out)
        {
            *out = task_queue_.front();
            task_queue_.pop();
        }
    };
    template <class T>
    ThreadPool<T> *ThreadPool<T>::ins = nullptr;
}
tcpServer.hpp
cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <functional>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <thread>
#include "log.hpp"
#include "thread_pool.hpp"
#include "Task.hpp"
using namespace std;

enum
{
    USAGE_ERROR = 1,
    SOCKET_ERROR = 2,
    BIND_ERROR = 3,
    LISTEN_ERROR = 4,
    ACCEPT_ERROR = 5,
    READ_ERROR = 6,
    WRITE_ERROR = 7
};
class TcpServer
{
private:
    uint16_t _port;     // 端口号
    int _listen_sockfd; // socket描述符(文件描述符)

public:
    TcpServer(const uint16_t &port)
        : _port(port),
          _listen_sockfd(-1)
    {
    }

    void InitServer()
    {

        // 1. 创建socket
        _listen_sockfd = socket(AF_INET, SOCK_STREAM, 0); // IPv4协议  TCP模式  默认值
        if (_listen_sockfd == -1)
        {
            logMessage(FATAL, "socket error");
            exit(SOCKET_ERROR);
        }
        logMessage(NORMAL, "Socket Create Success!");

        // 2. 绑定bind
        struct sockaddr_in local;           //  IPv4 网络地址结构体
        bzero(&local, sizeof(local));       // 清空结构体
        local.sin_family = AF_INET;         // 表示使用 IPv4 协议
        local.sin_port = htons(_port);      // 端口号 htons 主机字节序转网络字节序
        local.sin_addr.s_addr = INADDR_ANY; // 系统定义的宏(值为 0x00000000,对应 IPv4 地址 0.0.0.0)

        int n = bind(_listen_sockfd, (struct sockaddr *)&local, sizeof(local)); // 绑定socket与地址
        if (n < 0)
        {
            logMessage(FATAL, "bind error");
            exit(BIND_ERROR);
        }
        logMessage(NORMAL, "Bind Success!");

        // 3. 监听listen
        n = listen(_listen_sockfd, 5); // 最大连接数5
        if (n < 0)
        {
            logMessage(FATAL, "listen error");
            exit(LISTEN_ERROR);
        }
        logMessage(NORMAL, "Listening On Port " + to_string(_port) + " ...");
    }

    void Start()
    {
        while (true)
        {

            // 4. 与客户端连接accept
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int client_sockfd = accept(_listen_sockfd, (struct sockaddr *)&peer, &len); //
            if (client_sockfd < 0)
            {
                logMessage(ERROR, "accept error" + string(strerror(errno)));
                continue;
            }
            logMessage(NORMAL, "Get A New Client!, client_sockfd:" + to_string(client_sockfd));

            // 多线程
            // std::thread t(&TcpServer::serviceClient, this, client_sockfd, peer);
            // t.detach(); // 分离线程

            // 线程池
            ns_threadpool::ThreadPool<Task> *tp = ns_threadpool::ThreadPool<Task>::GetInstance(); // 获取线程池单例对象
            Task t(client_sockfd, peer);
            tp->PushTask(t);
        }
    }
    ~TcpServer()
    {
        if (_listen_sockfd != -1)
        {
            close(_listen_sockfd);
        }
    }
};
tcpServer.cpp
cpp 复制代码
#include <iostream>
#include <memory>
#include "tcpServer.hpp"
using namespace std;

// 输入  ./tcpServer  port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cerr << "Usages:" << argv[0] << " port" << endl;
        exit(USAGE_ERROR);
    }
    uint16_t _port = atoi(argv[1]);
    unique_ptr<TcpServer> tcpserver(new TcpServer(_port));
    tcpserver->InitServer();
    tcpserver->Start();
    return 0;
}

客户端

tcpClient.hpp
cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <functional>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

using namespace std;

class TcpClient
{
private:
    int _sockfd; // socket描述符(文件描述符)
    string _server_ip;
    uint16_t _server_port;

public:
    TcpClient(const string &server_ip, const uint16_t &server_port)
        : _server_ip(server_ip),
          _server_port(server_port),
          _sockfd(-1)
    {
    }
    void InitClient()
    {
        // 1. 创建socket
        _sockfd = socket(AF_INET, SOCK_STREAM, 0); // IPv4协议  TCP模式  默认值
        if (_sockfd == -1)
        {
            cerr << "socket error" << strerror(errno) << endl;
            exit(2);
        }
        cout << "Socket Create Success!" << endl;
    }

    void Start()
    {
        // 2. 连接connect
        struct sockaddr_in server;                              //  IPv4 网络地址结构体
        bzero(&server, sizeof(server));                         // 清空结构体
        server.sin_family = AF_INET;                            // 表示使用 IPv4 协议
        server.sin_port = htons(_server_port);                  // 端口号 htons 主机字节序转网络字节序
        server.sin_addr.s_addr = inet_addr(_server_ip.c_str()); // 将点分十进制的字符串 IP 转换为网络字节序的二进制形式
        int n = connect(_sockfd, (struct sockaddr *)&server, sizeof(server));
        if (n < 0)
        {
            cerr << "connect error" << strerror(errno) << endl;
            exit(3);
        }
        cout << "Connect To Server Success!" << endl;

        // 3. 主线程发送数据给服务器
        string message;
        while (true)
        {
            cout << "Please Enter# ";
            getline(cin, message);
            ssize_t s = send(_sockfd, message.c_str(), message.size(), 0);
            if (s < 0)
            {
                cerr << "send error" << strerror(errno) << endl;
                continue;
            }
            // 4.接收服务器回发的数据
            char buffer[1024];
            ssize_t n = recv(_sockfd, buffer, sizeof(buffer) - 1, 0);
            if (n > 0)
            {
                buffer[n] = 0; // 字符串结尾标志
                string echo_message = buffer;
                cout << "Get Message From Server: " << echo_message << endl;
            }
            else if (n == 0)
            {
                cout << "Server Quit!" << endl;
                break;
            }
            else
            {
                cerr << "read error" << strerror(errno) << endl;
            }
        }
    }
    ~TcpClient()
    {
        if (_sockfd != -1)
        {
            close(_sockfd);
        }
    }
};
tcpClient.cpp
cpp 复制代码
#include <iostream>
#include <memory>
#include "tcpClient.hpp"
using namespace std;

// 输入  ./tcpClient  server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        cerr << "Usages:" << argv[0] << " server_ip server_port" << endl;
        return 1;
    }
    string server_ip = argv[1];
    uint16_t server_port = stoi(argv[2]);
    unique_ptr<TcpClient> tcpclient(new TcpClient(server_ip, server_port));
    tcpclient->InitClient();
    tcpclient->Start();
    return 0;
}

运行结果

多线程:

线程池:

五、UDP与TCP对比

UDP通信特点

  • 无连接:通信前无需建立连接,直接发送数据。
  • 数据报导向:每次发送都是独立的数据包,需指定目标地址。
  • 服务器需绑定固定端口,客户端由系统自动分配端口。

TCP通信特点

  • 面向连接 :通信前需通过connect()建立连接(三次握手)。
  • 字节流导向:数据按顺序传输,无边界,需应用层处理粘包问题。
  • 可靠性:通过重传、确认机制保证数据不丢失、不重复。
  • 服务器需区分监听套接字_listen_sockfd)和连接套接字sockfd)。

UDP与TCP核心流程对比

流程阶段 UDP(无连接) TCP(面向连接)
套接字创建 socket(AF_INET, SOCK_DGRAM, 0) socket(AF_INET, SOCK_STREAM, 0)
服务器准备 bind() 绑定端口 bind()listen() 监听连接
客户端准备 直接发送(系统自动bind connect() 建立连接
数据传输 sendto() / recvfrom()(需指定地址) read() / write()(基于已连接套接字)
连接管理 无连接,每次通信独立 accept()接受连接,关闭时需close()

通过上述代码可以清晰看到,UDP流程更简单,适合简单通信;TCP流程更复杂,但提供可靠传输,适合需要数据完整性的场景。

六、网络命令

1.netstat:网络状态/连接查询工具

netstat 是传统的网络工具,用于查看系统的网络连接(TCP/UDP)、监听端口、路由表、接口统计等信息。

注意 :部分 Linux 发行版(如 CentOS 8、Ubuntu 20.04+)默认未安装 netstat,需通过 yum install net-tools(RHEL/CentOS)或 apt install net-tools(Ubuntu/Debian)安装。

bash 复制代码
netstat [选项]

常用选项(组合使用更高效)

选项 作用
-t 仅显示 TCP 连接/端口
-u 仅显示 UDP 连接/端口
-l 仅显示 监听状态 的端口(服务器常用,如 TCP 的 LISTEN 状态)
-n 数字形式 显示 IP 和端口(不反向解析域名/服务名,速度更快)
-p 显示占用端口的 进程 PID 和进程名(需 root 权限,核心实用选项)
-a 显示 所有 连接(监听+非监听,TCP+UDP)
-r 显示系统路由表(类似 route 命令)
-i 显示网络接口的统计信息(如收发数据包数、错误数)

高频使用示例

1.查看所有监听的 TCP 端口(服务器排查常用)

bash 复制代码
netstat -tln  # 组合:-t(TCP) + -l(监听) + -n(数字显示)

输出解释:

  • **Proto:**协议(tcp)
  • **Recv-Q:**接收队列(未处理的数据包数,过大可能表示服务繁忙)
  • **Send-Q:**发送队列(未发送的数据包数)
  • Local Address: 本地监听地址:端口(如 0.0.0.0:80 表示监听所有网卡的 80 端口,127.0.0.1:3306 表示仅本地监听 3306 端口)
  • Foreign Address: 远程地址(监听状态下为 0.0.0.0:*,表示等待任意远程连接)
  • **State:**状态(LISTEN 表示监听中)

2.查看所有 TCP 连接(含进程 PID/名称)

bash 复制代码
sudo netstat -tpn  # 需 root 权限(-p 选项),组合:-t(TCP) + -p(进程) + -n(数字)

用途:排查"哪个进程占用了某个端口",例如找到占用 80 端口的进程 PID 和名称(如 nginx、apache)。

3.查看指定端口的占用情况(精准排查)

结合 grep 过滤,例如查看 8080 端口的使用:

复制代码
sudo netstat -tlnp | grep 8080

4.查看所有 UDP 连接/端口

复制代码
netstat -un  # -u(UDP) + -n(数字显示)

UDP 是无连接协议,无 "LISTEN" 状态,仅显示已建立的 UDP 通信(如 DNS 解析、NTP 时间同步)。

2.pidof:快速查询进程 PID

pidof 是简单高效的工具,用于根据 进程名 直接查询对应的 PID(进程标识符),无需复杂过滤(对比 ps 命令更简洁)。

bash 复制代码
pidof [选项] 进程名

常用选项

选项 作用
-s 仅返回 一个 PID(若多个进程同名,返回第一个)
-x 包含 shell 脚本进程(默认仅匹配二进制进程)
-o 排除指定 PID(格式:-o 排除的PID
相关推荐
Black蜡笔小新1 小时前
国标设备如何在EasyCVR视频汇聚平台获取RTSP/RTMP流?
网络·ffmpeg·音视频
optimistic_chen1 小时前
【Docker入门】Docker原理和安装
linux·运维·服务器·docker·容器·命令行
wdfk_prog1 小时前
[Linux]学习笔记系列 --[drivers][base]devtmpfs
linux·笔记·学习
渣渣灰95871 小时前
Windows11安装WSL2(Windows Subsystem for Linux)
linux·运维·windows
南山二毛1 小时前
ubuntu开机自启动脚本
linux·运维·ubuntu
java干货1 小时前
用 MySQL SELECT SLEEP() 优雅模拟网络超时与并发死锁
网络·数据库·mysql
yi碗汤园2 小时前
【一文了解】网络请求
网络·unity
L1624762 小时前
nmcli 命令和手动修改网卡配置文件详细讲解(最后附带配置脚本参考学习)
服务器·网络·php
北京盟通科技官方账号2 小时前
Docker 容器化部署 EtherNet/IP 协议栈(ESDK):Windows 与国产银河麒麟 V10 实测对比
网络·网络协议·tcp/ip·docker·国产系统·ethernet/ip·工业协议
晚风吹人醒.2 小时前
Rsync多种传输方式实现远程同步,增量备份全流程讲解及示例
linux·运维·centos·rsync·远程同步·inotify·增量备份