Socket编程3个版本

Socket编程预备

数据传输到主机不是是目的吗?因为数据是给人用的。比如:聊天是人在聊天,浏览我网页是人在浏览? 但是人是怎么看到聊天信息的呢?怎么执行下载任务呢?怎么浏览网页信息呢?通过启动的qq,迅雷,浏览器。 而启动的qq,迅雷,浏览器都是进程。换句话说,进程是人在系统中的代表,只要把数据给进程,人就相当于就拿到了数据。 所以:数据传输到主机不是目的,而是手段。到达主机内部,再交给主机内的进程,才是目的(进程就是人在操作系统中的代表)

1.所以网络通信本质 = 跨主机的进程间通信,网络就充当进程间看到的同一份资源

2.上网只有两类 IO 行为

  • 下载:远端主机进程 → 网络 → 本机网卡 → 本机进程内存
  • 上传:本机进程内存 → 网卡 → 网络 → 远端主机进程 网络读写,本质属于IO 操作

但是系统中,同时会存在非常多的进程,当数据到达目标主机之后,怎么转发给目标进程?

认识端口号

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

• 端口号是⼀个 2 字节 16 位的整数;

• 端口号用来标识⼀个进程,告诉操作系统,当前的这个数据要交给哪⼀个进程来处理;

• ⼀个端口号只能被⼀个进程占用(⼀个进程可以绑定多个端口号)

又IP 地址:定位主机 ;端口号:定位主机内部的进程。 IP地址+端口号能够标识网络上的某⼀台主机的某⼀个进程

端口号范围划分

• 0 - 1023 :知名端口号,HTTP, FTP, SSH 等这些广为使用的应用层协议,他们的端口号都是固定的.(即我们自己不能使用这些端口号,就像110,120这些电话号码一样,我们不能使用)

• 1024 - 65535 :操作系统动态分配的端口号.客户端程序的端口号,就是由操作系统从这个范围分配的(客户端不用bind就是随机靠os分配)

PID(进程 ID) vs 端口号

我们之前在学习系统编程的时候,学习了pid可以表示唯⼀ 个进程;此处我们的端口号也是唯⼀表示⼀个进程,那么这两者之间是怎样的关系?

简单理解PID在操作系统内部标识进程;端口是在网络层标识进程

为什么不统一用pid?

解耦

  1. 如果网络使用 PID 寻址:进程管理和网络高度绑定。
    • 进程重启 PID 会变,无法稳定通信
    • PID 只是操作系统本地标识,不能跨主机传输(另一台主机的 PID 没有任何意义)。
  2. 端口号是标准网络字段,跨主机通用,不受本地进程调度影响。

理解: PID 是操作系统内部给程序的 "门牌号",只本机内核认识; 端口号是网络层面约定的 "对外门牌号",全世界主机都能识别。'

认识socket

在前面说了IP 地址:定位主机;端口号:定位主机内部的进程。IP地址+端口号能够标识网络上的某⼀台主机的某⼀个进程。所以socket=IP+端口号

通信的时候,本质是两个互联网进程代表⼈来进行通信,{srcIp,srcPort,dstIp,dstPort} 这样的4元组就能标识互联网中唯⼆的两个进程

传输层的典型代表

传输层是属于内核的,那么我们要通过网络协议栈进行通信,必定调用的是传输层提供的系统调用,来进行的网络通信

特性 TCP UDP
连接属性 有连接 无连接
可靠性 可靠传输 不可靠传输(不是贬义词,相比TCP做的更少,更简单)
数据形式 面向字节流(像接水,多少不确定,不完整) 面向数据报(一个一个的是完整的)
开销 头部大,机制复杂 头部极简,开销小
适用场景 文件传输、HTTP/HTTPS、SSH、数据库通信(要求数据准确) 直播、语音通话、游戏、DNS(允许少量丢包,追求低延迟)

这些特性后面再做理解,现在认识即可

网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分.那么如何定义网络数据流的地址呢?

• TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.

• 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据;所以 如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可;

可以调用以下库函数做网络字节序和主机字节序的转换(就是对IP地址和端口做大小端切换)

uint16_t → 用来存端口号(2 字节)
uint32_t → 用来存放 IP整型地址(4 字节)

小知识:uint32_t:无符号 32 位整数类型 (uint16_t同理),但uint32_t ≠ unsigned int,int 长度平台可变,所以就不一定是32位了,只有在int为32位的平台下才会sockaddr结构相等。

uint32_t 强制固定 32 位,跨平台大小统一,网络通信不会出错

记忆技巧:h=host 主机,n=net 网络,s=short (16 位),l=long (32 位)(eg:htonl就是h(主机)-->n(网络)的转化)

理解Socket 地址结构

首先我们先了解一下两大 IPC 体系区分

  1. System V IPC 信号量、消息队列、共享内存;仅支持本机进程通信,无法跨主机。
  2. POSIX Socket 一套接口两种用法:
  • 网络 Socket:跨主机通信;
  • 本地 Socket:本机进程通信

Socket 设计者提供统一接口 ,使用通用基类 struct sockaddr,兼容两种通信场景:

  1. AF_INET :IPv4 网络 Socket(跨主机网络通信)
  2. AF_UNIX :Unix 域本地 Socket(本机进程间通信)

实际写代码时:我们填充专用结构体,强制转换成 struct sockaddr* 传入函数。

struct sockaddr_un:

复制代码
struct sockaddr_un{
    sa_family_t sun_family;    // 地址族:AF_UNIX
    char sun_path[108];        // 本地文件路径名
};

struct sockaddr_in(简化版本):

复制代码
struct sockaddr_in{
    sa_family_t sin_family;    // 地址族:AF_INET
    uint16_t sin_port;        // 16位端口号
    struct in_addr sin_addr;  // 32位IPv4地址
    unsigned char sin_zero[8];// 8字节填充,对齐大小,兼容sockaddr
};

真实情况有多层封装

socket常见API:

所以如何区分通信类型?

依靠地址族字段判断:

复制代码
if(address->sa_family == AF_INET)
{
    // 网络通信,强转 sockaddr_in
}
else if(address->sa_family == AF_UNIX)
{
    // Unix本地通信,强转 sockaddr_un
}

socket () 函数

复制代码
int socket(int domain, int type, int protocol);

功能:创建通信端点,在内核生成 socket 对象,返回文件描述符 fd(理解:打开一个网络文件)

参数:

  1. domain(域) AF_INET和 AF_UNIX
  2. type(套接字类型)
  • SOCK_DGRAM:UDP
  • SOCK_STREAM:TCP
  1. protocol 协议编号,一般填 0,由 domain+type 就可以自动匹配是哪一种协议了。

返回值 成功:返回 socket 文件描述符(int); 失败:返回 -1,全局变量 errno 保存错误码。

bind () 函数

复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

功能:给 socket 文件描述符绑定「地址(IP + 端口)」,给套接字命名,让别人能找到你 。(socket() 仅仅创建出一个通信端点(拿到 fd),此时没有端口、没有 IP,外部无法主动连接 / 发数据给它。)

参数:

  • sockfd:socket () 创建得到的文件描述符;
  • *addr:通用地址结构体指针(sockaddr_in /sockaddr_un 强制转换传入);
  • addrlen:地址结构体占用字节大小

返回值 成功:返回 0; 失败:返回 -1,设置 errno

分场景理解要不要bind:

服务器(必须 bind):服务器需要固定端口(不会随机变化),这样客户端才能和服务端连接

客户端(一般不用手动 bind) :客户端发起连接时,操作系统自动随机分配临时端口

为什么?手动bind会怎样?

端口被占用问题

假如用了端口6666bind那么在程序异常退出时,其它程序都不能用6666这个端口号,即端口号被占用了

单进程就只能建立一条对外连接(严重限制)

源 IP + 源端口--->目标IP+目标端口建立1条连接,如果源端口固定,就只能建立1条连接了

sendto 发送数据

复制代码
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

参数

  1. sockfd:udp socket fd
  2. buf:要发的数据
  3. len:数据长度
  4. flags:填 0,采用系统默认阻塞 IO 行为
  5. dest_addr目标地址(你要发给谁)要强转
  6. addrlen:地址结构体大小

返回值

成功:发送的字节数;失败:-1

recvfrom 接收数据

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

参数

  1. sockfd:udp socket 文件描述符
  2. buf:存放收到数据的缓冲区
  3. len:缓冲区大小
  4. flags:一般填 0,采用系统默认阻塞 IO 行为
  5. src_addr:输出参数,自动保存发送方的地址(IP + 端口)要强转
  6. addrlen:输入输出,地址结构体长度(输入时告诉内核:你提供的地址缓冲区有多大 ,防止内核写地址时溢出。输出时内核会修改 *addrlen:填入本次实际返回的对端地址真实长度)

返回值

0:收到多少字节 -1:出错

1.udp sockfd,既可以读,又可以写。UDP 通信,其实是全双工的

全双工:通信双方可以同时收发数据,上行、下行相互独立,互不阻塞

为什么是全双工?

单个 UDP socket 文件描述符同时具备读缓冲区、写缓冲区 内核给每个 UDP 套接字分配两块独立缓冲区:

  • 接收缓冲区:存放别人发过来的数据 → recvf

    复制代码
    int inet_aton(const char *strptr, struct in_addr *addrptr);

    rom()读取

  • 发送缓冲区:存放你要发出去的数据 → sendto()发送 读写互不干扰。

IP 地址转换函数

知道 inet_addrinet_atoninet_ntoa有静态覆盖问题,不安全,只能支持IPv4

推荐用下面的2个,支持 IPv4+IPv6,兼容性最强

字符串 IP → 网络字节序

inet_pton

复制代码
int inet_pton(int family, const char *strptr, void *addrptr);

eg:

网络字节序→ 字符串 IP

inet_ntop

eg:

本地回环地址 127.0.0.1

  • 特点:仅本机内部通信
  • 数据路径:客户端发出的数据不会发送到物理网卡、不会进入局域网,操作系统内核内部转发给本机服务端。
  • 使用场景:只限本地调试网络代码,其它主机不能访问。

bind 的三种情况总结

  1. 不能 bind 公网 IP 云服务器的公网 IP 只是运营商做的映射,本机网卡并没有配置公网 IP,直接 bind 公网 IP 会失败。

  2. 可以 bind:127.0.0.1 或者本机内网 IP(如 10.0.0.6)

    • bind 127.0.0.1只允许本机客户端连接,外部机器无法访问
    • bind 内网 IP(10.0.0.6):同局域网其他机器 + 本机均可访问

如果服务端显式 bind 了某个特定 IP ,客户端访问时必须使用这个 IP ,否则连接失败。 例:server bind 10.0.0.6,客户端用127.0.0.1连接 → 访问不通!反过来同样不行

服务端推荐绑定 INADDR_ANY(0.0.0.0)

local.sin_addr.s_addr = INADDR_ANY

含义:监听本机所有网卡上的所有 IP 地址(不是说和能够与所有ip地址bind,只是能够监听到所有ip,可以./udpclient 公网 8080即监听到公网)

不需要手动指定单一 IP,规避上面 "IP 不匹配连不上" 的问题。

指令:netstat -anup

  • netstat:查看网络连接、端口监听(旧工具,推荐新版 ss
  • -a:显示所有连接
  • -n:数字形式展示
  • -u:只查看 UDP 套接字
  • -p:显示对应进程 PID、程序名

V1版本

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

相关头文件:

复制代码
#include <sys/types.h>     // 基础类型(早期必备,现在很多系统可省略)
#include <sys/socket.h>    // socket、bind、listen、accept、connect 函数 + 宏
#include <arpa/inet.h>     // sockaddr_in、inet_addr、inet_ntoa、inet_pton
#include <netinet/in.h>    // 定义 AF_INET、SOCK_STREAM
#include <unistd.h>        // close() 关闭文件描述符
#include <errno.h>         // errno 错误码
#include <string.h>        // memset 清空结构体

代码实现:

Udpserver.hpp:

复制代码
#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <strings.h> //bzero

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
using namespace std;
using namespace LogMoudle;
const int defaultid = -1;
using func_t = function<string &(string &)>;

// INADDR_ANY; ;  struct sockaddr_in local;
class Udpserver
{
public:
    Udpserver(uint16_t port, func_t fun)
        : _port(port), _fun(fun), _sockfd(defaultid), _isrunning(false)
    {
    }
    void Init()
    {
        // 1.创建socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd == -1)
        {
            LOG(LogLevel::FATAL) << "socket fail";
            exit(2);
        }
        LOG(LogLevel::INFO) << "socket sucess,socket: " << _sockfd;

        // 2.bind
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_port = htons(_port);
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = INADDR_ANY;
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success, sockfd : " << _sockfd;
    }
   
    void Start()
    {
        _isrunning=true;
        //我们平时用的程序app都是不会退出的,即都是死循环
        while(_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            bzero(&peer, sizeof(peer));
            socklen_t len=sizeof(peer);
            //收信息
            ssize_t r=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
            if(r>0)
            {
                buffer[r]=0;
                uint16_t peer_port=ntohs(peer.sin_port);
                string peer_ip=inet_ntoa(peer.sin_addr);
                LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port<< "]# " << buffer; // 1. 消息内容 2. 谁发的??
                //发信息
                string echo_string = "server echo@ ";
                echo_string += buffer;
                ssize_t s=sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&peer,len);
            }
            
        }
    }

    ~Udpserver() {}

private:
    uint16_t _port;
    bool _isrunning;
    int _sockfd;
    func_t _fun;
};

Udpserver.cc

复制代码
#include"UdpServer.hpp"
//.udpserver 8080
string&handler(string&s)
{
    s="hello"+s;
    return s;
}
int main(int argc,char *argv[])
{
    Enable_Console_Log_Strategy();
    if(argc!=2)
    {
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        return 1;
    }
    unique_ptr<Udpserver>uds=make_unique<Udpserver>(8080,handler);//这里传了handler但是没有使用,只做了server回显的操作
    uds->Init();
    uds->Start();

    return 0;
}

UdpClient.hpp:

复制代码
#include <iostream>
#include <string>
#include <functional>
#include <strings.h> //bzero
#include<string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
using namespace LogMoudle;
class Udpclient
{
public:
    Udpclient(string &ip, int port)
        : _sockfd(-1), _server_ip(ip), _server_port(port)
    {
    }
    ~Udpclient() {}
    void Init()
    {
        // socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd == -1)
        {
            LOG(LogLevel::FATAL) << "socket fail";
            exit(2);
        }
        LOG(LogLevel::INFO) << "socket sucess,socket: " << _sockfd;
        // bind,客户端不用手动bind,os自动分配端口号
    }
    void Start()
    {
        struct sockaddr_in server;
        memset(&server,0,sizeof(server));
        server.sin_family = AF_INET;
        server.sin_addr.s_addr = inet_addr(_server_ip.c_str());
        server.sin_port = htons(_server_port);
        while (true)
        {
            string buffer;
            cout << "Please Enter#" << endl;
            getline(cin, buffer);
            // 发送
            ssize_t s = sendto(_sockfd, buffer.c_str(), buffer.size(), 0, (struct sockaddr *)&server, sizeof(server));
            (void)s;
            //收信息
            char buffer2[1024];
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            ssize_t r=recvfrom(_sockfd,buffer2,sizeof(buffer2)-1,0,(struct sockaddr*)&peer,&len);
            if(r>0)
            {
                buffer2[r]=0;
                cout<<buffer2<<endl;
            }
            
        }
    }

private:
    string _server_ip;
    int _server_port;
    int _sockfd;
};

UdpClient.cc:

复制代码
#include"UdpClient.hpp"
//./udpclient ip port

int main(int argc,char *argv[])
{
    Enable_Console_Log_Strategy();

    if(argc!=3)
    {
        cerr<<"Usage:./udpclient ip port"<<endl;
        return 1;
    }
    string server_ip=argv[1];
    int server_port=stoi(argv[2]);
    unique_ptr<Udpclient>udc=make_unique<Udpclient>(server_ip,server_port);
    udc->Init();
    udc->Start();

    return 0;
}

makefile:

复制代码
.PHONY:all
all:udpclient udpserver

udpclient:UdpClient.cc
	g++ -o $@ $^ -std=c++17 #-static
udpserver:UdpServer.cc
	g++ -o $@ $^ -std=c++17

.PHONY:clean
clean:
	rm -f udpclient udpserver

V2:翻译版

Dict.hpp:

复制代码
#pragma once

#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include "Log.hpp"
#include "InetAddr.hpp"

const std::string defaultdict = "./dictionary.txt";
const std::string sep = ": ";

using namespace LogMoudle;
class Diet
{
public:
    Diet(const string &path = defaultdict)
        : _dict_path(path)
    {
    }
    bool LoadDict()
    {
        ifstream in(_dict_path);
        if (!in.is_open())
        {
            LOG(LogLevel::DEBUG) << "打开字典: " << _dict_path << " 错误";
            return false;
        }
        string line;
        while (getline(in, line))
        {
            auto pos = line.find(sep);
            if (pos == string::npos)
            {
                LOG(LogLevel::WARNING) << "解析" << line << "失败";
                continue;
            }
            string enlish = line.substr(0, pos);
            string chinese = line.substr(pos + 2);
            if (enlish.empty() || chinese.empty())
            {
                LOG(LogLevel::WARNING) << "没有有效内容" << line;
                continue;
            }
            _dict.insert({enlish, chinese});
            LOG(LogLevel::DEBUG) << "加载:" << line;
        }
        in.close();
        return true;
    }
    string Translate(const string &word, InetAddr &client)
    {
        auto iter = _dict.find(word);
        if (iter == _dict.end())
        {
            LOG(LogLevel::DEBUG) << "进入到了翻译模块, [" << client.Ip()<< " : " << client.Port() << "]# " << word << "->None";
            return "None";
        }
        LOG(LogLevel::DEBUG) << "进入到了翻译模块, [" << client.Ip()<< " : " << client.Port() << "]# " << word << "->" << iter->second;
        return iter->second;
    }
    ~Diet() {}

private:
    string _dict_path;
    unordered_map<string, string> _dict;
};

InetAddr.hpp:

复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
class InetAddr
{
public:
    InetAddr(struct sockaddr_in &addr)
        : _addr(addr)
    {
        _port = ntohs(_addr.sin_port);
        _ip = inet_ntoa(_addr.sin_addr);
    }
    string Ip()
    {
        return _ip;
    }
    uint16_t Port()
    {
        return _port;
    }
    string StringAddr()
    {
        return _ip+":"+to_string(_port);
    }
    ~InetAddr() {}

private:
    sockaddr_in _addr;

    uint16_t _port;
    string _ip;
};

UdpsServer.cc:

复制代码
#include"UdpServer.hpp"
#include"Dict.hpp"
//.udpserver 8080

int main(int argc,char *argv[])
{
    Enable_Console_Log_Strategy();
    if(argc!=2)
    {
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        return 1;
    }
    //
    Diet d;
   d.LoadDict();
   auto handler=[&d](const string&word,InetAddr&cli){return d.Translate(word,cli);};
    //2个模块连接的关键!!!
    unique_ptr<Udpserver>uds=make_unique<Udpserver>(8080,handler);
    uds->Init();
    uds->Start();

    return 0;
}

UdpServer.hpp:

复制代码
#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <strings.h> //bzero

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include"InetAddr.hpp"

using namespace std;
using namespace LogMoudle;
const int defaultid = -1;
using func_t = function<std::string(const std::string&, InetAddr&)>;
// INADDR_ANY; ;  struct sockaddr_in local;
class Udpserver
{
public:
    Udpserver(uint16_t port, func_t fun)
        : _port(port), _fun(fun), _sockfd(defaultid), _isrunning(false)
    {
    }
    void Init()
    {
        // 1.创建socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd == -1)
        {
            LOG(LogLevel::FATAL) << "socket fail";
            exit(2);
        }
        LOG(LogLevel::INFO) << "socket sucess,socket: " << _sockfd;

        // 2.bind
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_port = htons(_port);
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = INADDR_ANY;
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success, sockfd : " << _sockfd;
    }
   
    void Start()
    {
        _isrunning=true;
        //我们平时用的程序app都是不会退出的,即都是死循环
        while(_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            bzero(&peer, sizeof(peer));
            socklen_t len=sizeof(peer);
            //收信息
            ssize_t r=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
            if(r>0)
            {
                InetAddr client(peer);
                buffer[r]=0;
                string result=_fun(buffer,client);
             
             
                ssize_t s=sendto(_sockfd,result.c_str(),result.size(),0,(struct sockaddr*)&peer,len);
            }
            
        }
    }

    ~Udpserver() {}

private:
    uint16_t _port;
    bool _isrunning;
    int _sockfd;
    func_t _fun;
};

UdpClient.cc:

复制代码
#include"UdpClient.hpp"
//./udpclient ip port

int main(int argc,char *argv[])
{
    Enable_Console_Log_Strategy();

    if(argc!=3)
    {
        cerr<<"Usage:./udpclient ip port"<<endl;
        return 1;
    }
    string server_ip=argv[1];
    int server_port=stoi(argv[2]);
    unique_ptr<Udpclient>udc=make_unique<Udpclient>(server_ip,server_port);
    udc->Init();
    udc->Start();

UdpClient.hpp:

复制代码
#include <iostream>
#include <string>
#include <functional>
#include <strings.h> //bzero
#include<string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
using namespace LogMoudle;
class Udpclient
{
public:
    Udpclient(string &ip, int port)
        : _sockfd(-1), _server_ip(ip), _server_port(port)
    {
    }
    ~Udpclient() {}
    void Init()
    {
        // socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd == -1)
        {
            LOG(LogLevel::FATAL) << "socket fail";
            exit(2);
        }
        LOG(LogLevel::INFO) << "socket sucess,socket: " << _sockfd;
        // bind,客户端不用手动bind,os自动分配端口号
    }
    void Start()
    {
        struct sockaddr_in server;
        memset(&server,0,sizeof(server));
        server.sin_family = AF_INET;
        server.sin_addr.s_addr = inet_addr(_server_ip.c_str());
        server.sin_port = htons(_server_port);
        while (true)
        {
            string buffer;
            cout << "Please Enter#" << endl;
            getline(cin, buffer);
            // 发送
            ssize_t s = sendto(_sockfd, buffer.c_str(), buffer.size(), 0, (struct sockaddr *)&server, sizeof(server));
            (void)s;
            //收信息
            char buffer2[1024];
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            ssize_t r=recvfrom(_sockfd,buffer2,sizeof(buffer2)-1,0,(struct sockaddr*)&peer,&len);
            if(r>0)
            {
                buffer2[r]=0;
                cout<<buffer2<<endl;
            }
            
        }
    }

private:
    string _server_ip;
    int _server_port;
    int _sockfd;
};

V3:群聊版

InetAddr.hpp:

复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
class InetAddr
{
public:
    InetAddr(struct sockaddr_in &addr)
        : _addr(addr)
    {
        _port = ntohs(_addr.sin_port);
        _ip = inet_ntoa(_addr.sin_addr);
    }
    string Ip()
    {
        return _ip;
    }
    uint16_t Port()
    {
        return _port;
    }
     string StringAddr()
    {
        return "["+_ip+":"+to_string(_port)+"]";
    }
    bool operator==(const InetAddr&addr)
    {
         return addr._ip == _ip && addr._port == _port;
    }
    struct sockaddr_in NetAddr()
    {
        return _addr;
    }
    ~InetAddr() {}

private:
    sockaddr_in _addr;

    uint16_t _port;
    string _ip;
};

Route.hpp:

复制代码
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp"
#include "Log.hpp"
using namespace LogMoudle;
class Route
{
private:
    bool IsExist(InetAddr &peer)
    {
        for (auto &user : _online_user)
        {
            if (user == peer)
            {
                return true;
            }
        }
        return false;
    }
    void AddUser(InetAddr &peer)
    {
        LOG(LogLevel::INFO) << "新增一个在线用户" << peer.StringAddr();
        _online_user.push_back(peer);
    }
    void DeleteUser(InetAddr &peer)
    {
        for (auto iter = _online_user.begin(); iter != _online_user.end(); iter++)
        {
            if (*iter == peer)
            {
                LOG(LogLevel::INFO) << "删除一个在线用户" << peer.StringAddr() << "成功";
                _online_user.erase(iter);
                break;
            }
        }
    }

public:
    Route() {}
    void MessageRoute(int sockfd, string &message, InetAddr &peer)
    {
        LockGuard guard(_mutex);
        if (!IsExist(peer))
        {
            AddUser(peer);
        }
        string send_message = peer.StringAddr() + "#" + message;
        for (auto &user : _online_user)
        {
            // sendto(sockfd, send_message.c_str(), send_message.size(), 0, (struct sockaddr *)&(user.NetAddr()), sizeof(user.NetAddr()));
            // user.NetAddr() 是成员函数调用,返回值是临时对象(右值 rvalue)。
            // 不能对临时对象直接取地址 &
            struct sockaddr_in peer_addr = user.NetAddr();
            sendto(sockfd, send_message.c_str(), send_message.size(), 0,
                   (struct sockaddr *)&peer_addr, sizeof(peer_addr));
        }
        if (message == "QUIT")
        {
            LOG(LogLevel::INFO) << "删除一个在线用户: " << peer.StringAddr();
            DeleteUser(peer);
        }
    }
    ~Route() {}

private:
    vector<InetAddr> _online_user;
    Mutex _mutex;//说到底_online_user还是一个全局共享的,有些线程在删除有些在添加需要锁保护
};

UdpServer.hpp:

复制代码
#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <strings.h> //bzero

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include"InetAddr.hpp"

using namespace std;
using namespace LogMoudle;
const int defaultid = -1;
using func_t2 = std::function<void(int sockfd, const std::string&, InetAddr&)>;

class Udpserver
{
public:
    Udpserver(uint16_t port, func_t2 fun)
        : _port(port), _fun(fun), _sockfd(defaultid), _isrunning(false)
    {
    }
    void Init()
    {
        // 1.创建socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd == -1)
        {
            LOG(LogLevel::FATAL) << "socket fail";
            exit(2);
        }
        LOG(LogLevel::INFO) << "socket sucess,socket: " << _sockfd;

        // 2.bind
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_port = htons(_port);
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = INADDR_ANY;
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success, sockfd : " << _sockfd;
    }
   
    void Start()
    {
        _isrunning=true;
        //我们平时用的程序app都是不会退出的,即都是死循环
        while(_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            bzero(&peer, sizeof(peer));
            socklen_t len=sizeof(peer);
            //收信息
            ssize_t r=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
            if(r>0)
            {
                InetAddr client(peer);
                buffer[r]=0;
                _fun(_sockfd,buffer,client);//buffer是const char*
              
            }
            
        }
    }

    ~Udpserver() {}

private:
    uint16_t _port;
    bool _isrunning;
    int _sockfd;
    func_t2 _fun;
};

UdpServer.cc:

复制代码
#include "UdpServer.hpp"
#include "Route.hpp"
#include "ThreadPool.hpp"
#include<memory>
//.udpserver 8080
using namespace ThreadPoolModule;
using task_t = function<void()>;
int main(int argc, char *argv[])
{
    Enable_Console_Log_Strategy();
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        return 1;
    }
    //
    uint16_t port = stoi(argv[1]);


    Enable_Console_Log_Strategy();
    auto tp = ThreadPool<task_t>::GetInstance();
    Route r;
//最关键的地方,不同模块之间的连接
    unique_ptr<Udpserver> usvr = make_unique<Udpserver>(port,[&r,&tp](int sockfd,const string &message, InetAddr&peer){
        task_t t=std::bind(&Route::MessageRoute,&r,sockfd,message,peer);
        tp->Enqueue(t);
//std::bind( 函数地址, 对象指针, 参数1, 参数2, 参数3 ... );
//成员函数要this指针,并且Route::MessageRoute不是地址要加&和普通函数不一样
    });
    usvr->Init();
    usvr->Start();

    return 0;
}

UdpClient.cc:

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


#include"pthread.hpp"
//./udpclient ip port
using namespace ThreadModlue;
string server_ip;
int server_port;
uint16_t sockfd;
int id;
void Send()
{
    while (1)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_addr.s_addr = inet_addr(server_ip.c_str());
        server.sin_port = htons(server_port);

        const std::string online = "inline";
        sendto(sockfd, online.c_str(), online.size(), 0, (struct sockaddr *)&server, sizeof(server));

        while (true)
        {
            string buffer;
            cout << "Please Enter#" ;
            getline(cin, buffer);
            // 发送
            ssize_t s = sendto(sockfd, buffer.c_str(), buffer.size(), 0, (struct sockaddr *)&server, sizeof(server));
            (void)s;
            if (buffer == "QUIT")
            {
                pthread_cancel(id);
                break;
            }
        }
    }
}
void Recv()
{
    while (true)
    {
        char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
        if (m > 0)
        {
            buffer[m] = 0;
            std::cerr << buffer << std::endl; // 为什么要用cerr因为对应2号文件,即只会显示收到的信息,不会出现,tty看当前处于哪一个终端
            //"Please Enter#"
        }
    }
    
}
int main(int argc, char *argv[])
{
    Enable_Console_Log_Strategy();

    if (argc != 3)
    {
        cerr << "Usage:./udpclient ip port" << endl;
        return 1;
    }
    // 1. 创建socket
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        return 2;
    }
    server_ip = argv[1];
    server_port = stoi(argv[2]);

    // 2. 创建线程,为什么要创建2个线程,因为只有一个线程的话先发信息如果没有发的话就会一直卡住,不会收到信息
    
    Thread recver(Recv);
    Thread sender(Send);

    recver.Start();
    sender.Start();

    id = recver.Id();

    recver.Join();
    sender.Join();
    

    return 0;
}
相关推荐
大鱼>1 小时前
DSPy:LLM程序自动编译与提示词优化
开发语言·人工智能·python·深度学习
uncle_ll1 小时前
服务器选型、微调范式、训练优化与环境搭建
服务器·python·gpt·llm·nlp
撩妹帝九歌1 小时前
Java文件写入与编码、字节数组、字符集、字符编解码 一文打通!
java·开发语言
kaixin_learn_qt_ing1 小时前
单网卡绑定多个 IP 地址
网络·网络协议·tcp/ip
2601_965798471 小时前
Build a Consulting Site in 2026: Nifty Theme Practical Review
java·linux·数据库
星核0penstarry1 小时前
Solon AI v4.0.4 技术分析:Java Agent 框架的兼容性突破与选型考量 基于 OSCHINA 2026-07-30 报道及公开技术资料整理
java·开发语言·人工智能
加油码1 小时前
共享内存详解:原理、系统调用与高性能进程间通信
linux·c语言·c++
昨夜星河入梦来1 小时前
postman接口测试报错503的解决方法
开发语言·postman
枳实-叶1 小时前
Linux 环境下段错误出现的原因及调试方法
linux·运维·服务器
for_ever_love__2 小时前
iOS: 网络请求第三方库的使用: AFNetworking
网络·学习·ios·objective-c·cocoa·xcode