网络套接字1

网络套接字1

📟作者主页:慢热的陕西人

🌴专栏链接:Linux

📣欢迎各位大佬👍点赞🔥关注🚓收藏,🍉留言

本博客主要内容讲解了udp的Linux环境下的使用,以及网络的一些基础知识

文章目录

  • 网络套接字1
    • 1.预备知识
      • 1.1理解源IP地址和目的IP地址
      • 1.2认识端口号
      • 1.3理解"端口号"和"进程ID"
      • 1.4理解源端口号和目的端口号
      • 1.5认识TCP协议
      • 1.6认识UDP
      • 1.7网络字节序
    • 2.socket编程接口
      • 2.1socket常见API
      • 2.2sockaddr结构
    • 3.实现一个C/S示例代码
      • 3.1V1
      • 3.2V2
      • 3.3V3
      • 3.4V4版本
      • 3.5提供一个Windows客户端
      • 3.6客户端和服务端的源码:

1.预备知识

1.1理解源IP地址和目的IP地址

唐僧例子1

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址 .

思考: 我们光有IP地址就可以完成通信了嘛? 想象一下发qq消息的例子, 有了IP地址能够把消息发送到对方的机器上,但是还需要有一个其他的标识来区分出, 这个数据要给哪个程序进行解析 。

1.2认识端口号

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

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

1.3理解"端口号"和"进程ID"

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

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

1.4理解源端口号和目的端口号

传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要
发给谁”;

1.5认识TCP协议

此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再详细讨论TCP的一些细节问题

  • 传输协议层
  • 有连接
  • 可靠传输
  • 面向字节流

1.6认识UDP

此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识; 后面再详细讨论

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

1.7网络字节序

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

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址;
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节 ;
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
  • 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

2.socket编程接口

2.1socket常见API

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);

2.2sockaddr结构

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

image-20240308215923728

  • 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结构体指针做为参数。

3.实现一个C/S示例代码

3.1V1

准备工作:网络套接字的创建和对应的端口绑定

        void InitServer()
        {
            //1.创建socket接口,打开网络文件
            //数据报套接字
            sock_ = socket(AF_INET, SOCK_DGRAM, 0);
            //返回的是一个网络文件的文件描述符
            if(sock_ < 0)
            {
                std::cerr << "create socket error" << strerror(errno) << std::endl;
                exit(SOCKET_ERR); //用枚举构造的一个退出码
            }
            std::cout << "create socket success:" << sock_ << std::endl;

            //2.给服务器指明IP地址和端口号port
            struct sockaddr_in local; //这个local,定义在用户空间特定的函数栈帧上,而不是内核里
            bzero(&local, sizeof(local)); //将local初始化为0

            //对local进行赋值
            local.sin_family = AF_INET;  //指定为Address_Family
            local.sin_port = htons(port_); //将主机字节序的port转化为对应的网络字节序

            // inet_addr: 1,2两个功能都具备
            // 1. 字符串风格的IP地址,转换成为4字节int, "1.1.1.1" -> uint32_t -> 能不能强制类型转换呢?不能,这里要转化
            // 2. 需要将主机序列转化成为网络序列
            // 3. 云服务器或者一般的服务器,我们是不需要确定的ip的,
            local.sin_addr.s_addr = INADDR_ANY; //让我们的udp_server在启动的时候,可以绑定任意的本机IP

            if(bind(sock_, (struct sockaddr*)&local, sizeof(local)) < 0)  //绑定端口号
            {
                std::cerr << "bind socket error" << strerror(errno) << std::endl;
                exit(BIND_ERR);
            }

            std::cout << "bind socket success:"<< sock_ << std::endl;


        }

v1版本中我们发现我们的服务端在绑定的时候却出错了!这是为什么呢?

云服务器不需要,bind对应的ip地址,需要让服务器自己指定IP地址; 我们自己本地的虚拟机或者物理机是支持的。

[mi@lavm-5wklnbmaja udpv1]$ ./udp_server 
server addr1.1.1.1:8082
create socket success
bind socket errorCannot assign requested address

我们如此去处理:

并且因为INADDR_ANY是一个缺省的零值,我们并不需要对他进行主机转网络化。

// 3. 云服务器或者一般的服务器,我们是不需要确定的ip的,
local.sin_addr.s_addr = INADDR_ANY; //让我们的udp_server在启动的时候,可以绑定任意的本机IP

接下来完成收发消息:

收消息的接口:

ssize_t recvfrom(int socket, void *restrict buffer, size_t length,int flags, struct sockaddr *restrict address,
      socklen_t *restrict address_len);
  1. socket:这是已建立连接的套接字文件描述符,用于接收数据。

  2. buffer:这是指向用于存储接收数据的缓冲区的指针。

  3. length:指定接收数据的最大长度。

  4. flags

    :用于控制接收数据的方式。常见选项包括:

    • MSG_WAITALL:阻塞等待,直到接收到指定长度的数据。
    • MSG_DONTWAIT:非阻塞模式,如果没有数据可读,立即返回-1。
  5. address:这是指向 struct sockaddr 结构的指针,用于存储发送方的地址信息。

  6. address_len:表示 address 结构的字节数。

需要注意的是,recvfrom 函数在 UDP 协议中返回长度为 0 的数据是可行的。因为在 UDP 的情况下,它会形成 20 字节的 IP 首部(IPv4)和一个 8 字节的 UDP 首部,而没有数据的 IP 数据报。因此,UDP 是无连接的协议,不需要真正的发送缓冲区。

发消息的接口:

ssize_t sendto(int socket, const void *message, size_t length,int flags, const struct sockaddr *dest_addr,
              socklen_t dest_len);

sendto 函数用于将数据从套接字发送给目标主机。

  1. socket:这是已建立连接的套接字文件描述符,用于发送数据。

  2. message:这是指向要发送数据的缓冲区的指针。

  3. length:指定要发送数据的长度。

  4. flags

    :用于控制发送数据的方式。常见选项包括:

    • MSG_DONTWAIT:非阻塞模式,如果无法立即发送数据,立即返回-1。
  5. dest_addr:这是指向 struct sockaddr 结构的指针,用于指定目标主机的地址信息。

  6. dest_len:表示 dest_addr 结构的字节数。

需要注意的是,sendto 函数在 UDP 协议中不需要建立连接,因此不需要经过连接操作。它专门为 UDP 协议提供,因此参数中包含了目标地址信息。

我们完成服务端的收消息和发消息模块:

        void Start()
        {
            char buffer[1024]; //我们规定网络通信的时候最大时候是1024字节
            while(true)
            {
                //收消息
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer); // 这里一定要写清楚,未来缓冲区的大小
                int n = recvfrom(sock_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); // 这里的-1为了兼容C向字符串的结尾加\0
                if(n > 0) buffer[n] = '\0'; //读取成功,设置\0
                else continue; //否则的话继续读取

                //拿到地址和端口,并且完成网络转主机化
                std::string clientip = inet_ntoa(peer.sin_addr);
                uint16_t clientport = ntohs(peer.sin_port); 
                std::cout <<clientip << "-"<< clientport << "#get massege : " << buffer << std::endl;
                
                //发消息
                sendto(sock_, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, sizeof(peer));
            }
        }

我们运行以后,可以用netstat -nuap的指令去查看我们对应的当前在Linux主机上运行的网络服务:

[root@lavm-5wklnbmaja test_db]# netstat -nuap
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
udp        0      0 0.0.0.0:68              0.0.0.0:*                           2658/dhclient       
udp        0      0 172.16.0.3:123          0.0.0.0:*                           1845/ntpd           
udp        0      0 127.0.0.1:123           0.0.0.0:*                           1845/ntpd           
udp        0      0 0.0.0.0:123             0.0.0.0:*                           1845/ntpd           
udp        0      0 172.16.0.3:47847        169.254.169.240:53      ESTABLISHED 11484/nscd          
udp        0      0 0.0.0.0:8081            0.0.0.0:*                           29515/./udp_server  
udp6       0      0 fe80::d662:97b7:397:123 :::*                                1845/ntpd           
udp6       0      0 ::1:123                 :::*                                1845/ntpd           
udp6       0      0 :::123                  :::*                                1845/ntpd    

完成对应的客户端:

static void Usage(string proc)
{
    cout << "Usage:\n\t" << proc << "serverip  serverport  "<< "port\n" << endl;
}

//./udp_client serverip serverport
int main(int argc, char* argv[])
{

    if(argc != 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    string serverip = argv[1];  //获取ip
    uint16_t serverport = atoi(argv[2]);  //获取port
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if(sock < 0)
    {
        std::cerr << "create socket error " << std::endl;
        exit(SOCKET_ERR);
    }

    //client 这里要不要bind呢?要的!
    //要不要自己bind呢?但是我们不需要自己bind,也不要自己bind,操作系统会帮我们bind。
    //为什么?因为我们要做到每个客户端的端口号不重复,也就是端口号的冲突问题,直接交给OS
    //那么server为什么要自己绑定?1.因为server的端口不能随意改变,众所周知且不能被改变的
    //同一家公司的port号,需要统一和规范的

    //明确server是谁
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server)); //将server内部的数据全初始化为0
    server.sin_family = AF_INET; //指定我们的网络模式为ipv4
    server.sin_port = htons(serverport); //将端口号主机转网络然后赋值给server
    server.sin_addr.s_addr = inet_addr(serverip.c_str()); //将字符串类型的ip转化为对应的用.分割的格式

    while(true)
    {


        //用户输入消息
        string message;
        cout << "Please Enter# ";
        cin >> message;

        //什么时候bind? 在我们首次系统调用发送数据的时候,OS底层会随机选择clientport+自己的IP, 1.bind 2.构建发送数据的报文
        //发送给服务端
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));


        //收
        char buffer[1024];
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        int n = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);
        if(n > 0) // 收到了数据
        {
            buffer[n] = '\0';
            cout << "server echo# " << buffer << endl;
        }
    }
    return 0;
}

这里我们进行远端测试的时候,要先打开自己服务器上udp的端口防火墙,要不然会测试失败。

3.2V2

我们的v1版本实现的是网络的IO问题,那么我们接下来要实现的是要对我们服务器收到的消息进行业务处理

1.做字符串处理,小写转大写

std::string transactionString(std::string requets)
{
    std::string result;
    char c;
    for(auto& r : requets)
    {
        if(islower(r))
        {
            c = toupper(r);
            result.push_back(c);
        }
        else
        result.push_back(r);
    }
    
    return result;
}

2.做命令处理

在本地把命令发给服务器,让服务器在本地执行,并且把执行结果返回给客户端

这里我们需要用到popen接口:

FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
  1. popen(const char *command, const char *type)
    • 参数:
      • command:一个字符串,表示要执行的命令或可执行程序的名称。
      • type:一个字符,指定打开方式。它只能是 'r''w',分别表示读取命令的标准输出或写入命令的标准输入。
    • 功能:
      • popen() 函数创建一个管道,并通过 fork 或者启动一个子进程来执行指定的 command
      • 如果 type'r',则从应用程序的标准输出读取内容并返回一个文件指针。
      • 如果 type'w',则将数据传递给应用程序的标准输入。
      • popen() 的返回值是一个文件指针,成功时非空,失败时为 NULL
  2. pclose(FILE *stream)
    • 参数:
      • stream:先前由 popen() 返回的文件指针。
    • 功能:
      • pclose() 用于关闭由 popen() 创建的管道和文件指针。
      • 成功时,返回子进程的终止状态;失败时,返回 -1,错误原因存储在 errno 中。
bool isPass(const std::string& command)
{   
    bool pass = true;

    auto pos = command.find("mv");
    if(pos != std::string::npos) pass = false;

    pos = command.find("rm");
    if(pos != std::string::npos) pass = false;

    // pos = command.find("");
    // if(pos != std::string::npos) pass = false;

    return pass;
}


std::string excuteCommend(const std::string& command)
{
    //1.安全检查
    if(!isPass(command)) return "Bad command!";

    //2.业务处理
    FILE* fp = popen(command.c_str(), "r");
    if(fp == nullptr) return "None"; 
    //3.获取结果
    char line[1024];
    std::string result;
    while(fgets(line, sizeof(line), fp) != NULL)
    {
        result += line;
    }

    pclose(fp);

    return result;
}

3.3V3

将我们服务器收到的每一个消息,发送给每一个服务端,模拟一个群聊的功能:

那么过程中有人收消息,有人发消息,类似于一个cp模型:所以我们直接使用之前使用过的一个环形队列的模型;

那么为了维护唤醒队列的线程安全,我们引入之前实现的LockGuard

同时我们引入了Thread.hpp

思路:主要的思路是,每当一个客户端向服务端发送消息的时候,

发的进程:先在map内部去检测,查看当前的用户是否在map内部,如果不在,我们就push到map,然后将消息插入到我们的环形队列中。

收的进程:先从环形队列中取出收到的消息,然后将map中的peer去出放在一个vector中(这个过程要保证线程安全),最后在遍历vector,将消息广播出去,也就是发给曾经给服务器发过消息的客户端。

//RingQueue.hpp

const int N = 50;

#include<vector>
#include<semaphore.h>


using namespace std;

template<class T>
class RingQueue
{

private:
    //P操作
    void P(sem_t &m)
    {
        sem_wait(&m);
    }
    //V操作
    void V(sem_t &m)
    {
        sem_post(&m);
    }

    void lock(pthread_mutex_t &m)
    {
        pthread_mutex_lock(&m);
    }

    void unlock(pthread_mutex_t &m)
    {
        pthread_mutex_unlock(&m);
    }
public:
    RingQueue(int num = N):_cap(num),_v(num) ,_start_step(0), _end_step(0)
    {
        sem_init(&_data_sem, 0, 0);  //数据初始量为零
        sem_init(&_space_sem, 0, _cap); //空间初始量为_cap
        pthread_mutex_init(&_con_mutex, nullptr);
        pthread_mutex_init(&_pro_mutex, nullptr);
    }
    void push(const T& in)
    {
        P(_space_sem);     //p操作之后不需要判断是否有资源,因为p操作通过之后一定有资源
        
        lock(_pro_mutex);
        _v[_end_step++] = in;
        _end_step %= _cap;
        unlock(_pro_mutex);

        V(_data_sem);
    }

    void pop(T* out)
    {
        P(_data_sem);

        lock(_con_mutex);
        *out = _v[_start_step++];
        _start_step %= _cap;
        unlock(_con_mutex);

        V(_space_sem);
    }

    ~RingQueue()
    {
        sem_destroy(&_data_sem);
        sem_destroy(&_space_sem);
        pthread_mutex_destroy(&_con_mutex);
        pthread_mutex_destroy(&_pro_mutex);
    }

private:
    vector<T> _v;  //用于模拟环形队列
    sem_t _data_sem; //表示数据的信号量
    sem_t _space_sem; //表示空间的信号量
    int _cap;                //表示环形队列的大小
    int _start_step;        //表示消费者的位置
    int _end_step;          //表示生产者的位置

    pthread_mutex_t _con_mutex;
    pthread_mutex_t _pro_mutex;

};

//LockGuard.hpp

#pragma


#include<mutex>

class Mutex
{
public:
    Mutex(pthread_mutex_t* pthread) : _pthread(pthread)
    {}

    void lock()
    {
        pthread_mutex_lock(_pthread);
    }

    void unlock()
    {
        pthread_mutex_unlock(_pthread);
    }

    ~Mutex()
    {}
private:
    pthread_mutex_t * _pthread;

};


class LockGuard
{
public:

    LockGuard(pthread_mutex_t* mutex) :_mutex(mutex)
    {
        _mutex.lock();
    }

    ~LockGuard()
    {
        _mutex.unlock();
    }

private:
    Mutex _mutex;
};

//Thread.hpp

#pragma once

#include<pthread.h>
#include<unistd.h>
#include<string>


class Thread
{

public:
    typedef enum
    {
        NEW = 0,
        RUNING,
        EXIT
    }ThreadStatus;

   // typedef void* (*func_t)(void*);
    using func_t = std::function<void()>;

    Thread(int mum, func_t func) :_tid(0), _status(NEW), _func(func)
    {
        char name[128];
        snprintf(name, sizeof name, "thread-%d", _tid);
        _name = name;
    }

    int status() {  return _status; }
    string thread_name(){   return _name; }

    pthread_t threadid()
    {
        if(_status == RUNING)
        {
            return _tid;
        }

        else return 0;
    }

    static void* runHelper(void* args)
    {
        Thread* ts = (Thread*) args; //通过这种方式拿到对象
        (*ts)();
        return nullptr;
    }

    void operator()()
    {
        if(_func != nullptr) _func(); //使用仿函数的方式运行对应的线程进入函数
    }

    void run()
    {
        int n = pthread_create(&_tid, nullptr, runHelper, this);
        if(n != 0) exit;
        _status = RUNING;
    }

    void join()
    {
        int n = pthread_join(_tid, nullptr);

        if(n != 0)
        {
            cerr << "main thread join error:"  << _name << endl;
            return ;
        }
        _status = EXIT;
    }

    ~Thread()
    {}


public:
    pthread_t _tid;
    string _name;
    func_t _func; //线程未来要执行的回调
    ThreadStatus _status;
};

3.4V4版本

客户端线程化:构造一个线程专门去执行们收消息的任务

//udp_client.cc

#include"udp_client.hpp"


#include<pthread.h>


using namespace std;


static void Usage(string proc)
{
    cout << "Usage:\n\t" << proc << "serverip  serverport  "<< "port\n" << endl;
}


void* reciver(void* args)
{
    int sock = *(static_cast<int *>(args)); // 获取到对应的套接字

    while(true)
    {
        // 收
        char buffer[2048];
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        int n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);
        if (n > 0) // 收到了数据
        {
            buffer[n] = '\0';
            cout << buffer << endl;
        }
    }

    return nullptr;
}



//./udp_client serverip serverport
int main(int argc, char* argv[])
{

    if(argc != 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    string serverip = argv[1];  //获取ip
    uint16_t serverport = atoi(argv[2]);  //获取port
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if(sock < 0)
    {
        std::cerr << "create socket error " << std::endl;
        exit(SOCKET_ERR);
    }

    //client 这里要不要bind呢?要的!
    //要不要自己bind呢?但是我们不需要自己bind,也不要自己bind,操作系统会帮我们bind。
    //为什么?因为我们要做到每个客户端的端口号不重复,也就是端口号的冲突问题,直接交给OS
    //那么server为什么要自己绑定?1.因为server的端口不能随意改变,众所周知且不能被改变的
    //同一家公司的port号,需要统一和规范的

    //明确server是谁
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server)); //将server内部的数据全初始化为0
    server.sin_family = AF_INET; //指定我们的网络模式为ipv4
    server.sin_port = htons(serverport); //将端口号主机转网络然后赋值给server
    server.sin_addr.s_addr = inet_addr(serverip.c_str()); //将字符串类型的ip转化为对应的用.分割的格式

    pthread_t tid;
    pthread_create(&tid, nullptr, &reciver, &sock);

    while(true)
    {
        //用户输入消息
        string message;
        std::cerr << "Please Enter Your Message#";
        //cin >> message;

        getline(cin, message);

        //什么时候bind? 在我们首次系统调用发送数据的时候,OS底层会随机选择clientport+自己的IP, 1.bind 2.构建发送数据的报文
        //发送给服务端
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));

    }


    pthread_join(tid, nullptr);

    return 0;
}

3.5提供一个Windows客户端

我们这里实现一个windows的客户端,最终实现之后可以和Linux端一同通信。

代码只有四处不同:

#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
#include<string>
#include<winsock2.h>


#pragma comment(lib, "ws2_32.lib") //引入windows下的库

using namespace std;

int main()
{
	//用于检查
	WSADATA WSAData;
	if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0)
	{
		printf("init error");
		return -1;
	}


	//....


	//用于结束对套接字库的使用。其主要目的是以恰当的方式关闭和清理套接字编程库
	//对于每个成功的WSAStartup调用,都必须有一个相应的WSACleanup调用,否则会导致资源泄漏
	WSACleanup();

	return 0;
}

完整代码:

#include<iostream>
#include<string>
#include<winsock2.h>
#include <WS2tcpip.h> // for inet_pton
#include <thread>



#pragma comment(lib, "ws2_32.lib") //引入windows下的库

using namespace std;

int64_t serverport = 8888;

string serverip = "117.72.37.100";




void ReceiveData(SOCKET sock) {
    char buffer[2048];
    sockaddr_in temp;
    int len = sizeof(temp);
    while (true) {
        int n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&temp, &len);
        if (n > 0) {
            buffer[n] = '\0';
            cout << buffer << endl;
        }
    }
}

int main()
{
	//用于检查
	WSADATA WSAData;
	if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0)
	{
		printf("init error");
		return -1;
	}

    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "create socket error " << std::endl;
        exit(-2);
    }

    //client 这里要不要bind呢?要的!
    //要不要自己bind呢?但是我们不需要自己bind,也不要自己bind,操作系统会帮我们bind。
    //为什么?因为我们要做到每个客户端的端口号不重复,也就是端口号的冲突问题,直接交给OS
    //那么server为什么要自己绑定?1.因为server的端口不能随意改变,众所周知且不能被改变的
    //同一家公司的port号,需要统一和规范的

    //明确server是谁
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server)); //将server内部的数据全初始化为0
    server.sin_family = AF_INET; //指定我们的网络模式为ipv4
    server.sin_port = htons(serverport); //将端口号主机转网络然后赋值给server
    //server.sin_addr.s_addr = inet_addr(serverip.c_str()); //将字符串类型的ip转化为对应的用.分割的格式
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));

    thread receiveThread(ReceiveData, sock);

    cout << "Please Enter Your Message#";

    while (true)
    {
        //用户输入消息
        string message;

        //cin >> message;

        getline(cin, message);

        //什么时候bind? 在我们首次系统调用发送数据的时候,OS底层会随机选择clientport+自己的IP, 1.bind 2.构建发送数据的报文
        //发送给服务端
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));

    }

    receiveThread.join();

	//用于结束对套接字库的使用。其主要目的是以恰当的方式关闭和清理套接字编程库
	//对于每个成功的WSAStartup调用,都必须有一个相应的WSACleanup调用,否则会导致资源泄漏
	WSACleanup();



	return 0;
}

3.6客户端和服务端的源码:

  • server.hpp
#pragma once

#include<iostream>
#include<cstring>
#include<strings.h>
#include<string>
#include<cstdio>
#include<functional>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unordered_map>
#include<pthread.h>
#include<vector>
#include"err.hpp"
#include"RingQueue.hpp"
#include"LockGuard.hpp"
#include"Thread.hpp"

namespace ns_server
{

    const static uint16_t default_port = 8080; //设定一个端口号的默认值
    using func_t = std::function<std::string(std::string)>; //使用函数包装器,包装一个返回值和参数的都是string类型的函数

    class UdpServer
    {
    public:
        UdpServer(uint16_t port = default_port):port_(port)
        {
            std:: cout << "server addr" << port_ << std::endl;
            pthread_mutex_init(&lock, nullptr);


            p = new Thread(1, std::bind(&UdpServer::Recv, this));
            c = new Thread(2, std::bind(&UdpServer::Broadcast, this));
        }

        void start()
        {
            //1.创建socket接口,打开网络文件
            //数据报套接字
            sock_ = socket(AF_INET, SOCK_DGRAM, 0);
            //返回的是一个网络文件的文件描述符
            if(sock_ < 0)
            {
                std::cerr << "create socket error" << strerror(errno) << std::endl;
                exit(SOCKET_ERR); //用枚举构造的一个退出码
            }
            std::cout << "create socket success:" << sock_ << std::endl;

            //2.给服务器指明IP地址和端口号port
            struct sockaddr_in local; //这个local,定义在用户空间特定的函数栈帧上,而不是内核里
            bzero(&local, sizeof(local)); //将local初始化为0

            //对local进行赋值
            local.sin_family = AF_INET;  //指定为Address_Family
            local.sin_port = htons(port_); //将主机字节序的port转化为对应的网络字节序

            // inet_addr: 1,2两个功能都具备
            // 1. 字符串风格的IP地址,转换成为4字节int, "1.1.1.1" -> uint32_t -> 能不能强制类型转换呢?不能,这里要转化
            // 2. 需要将主机序列转化成为网络序列
            // 3. 云服务器或者一般的服务器,我们是不需要确定的ip的,
            local.sin_addr.s_addr = INADDR_ANY; //让我们的udp_server在启动的时候,可以绑定任意的本机IP

            if(bind(sock_, (struct sockaddr*)&local, sizeof(local)) < 0)  //绑定端口号
            {
                std::cerr << "bind socket error" << strerror(errno) << std::endl;
                exit(BIND_ERR);
            }

            std::cout << "bind socket success:"<< sock_ << std::endl;

            p->run();
            c->run();
        }

        void addUser(const std::string& name, const struct sockaddr_in& peer)
        {

            LockGuard lockguard(&lock);
            auto iter = onlineuser.find(name);
            if(iter == onlineuser.end())
            onlineuser.insert(std::pair<const std::string, const struct sockaddr_in>(name , peer));
        }

        void Recv()
        {
            char buffer[1024]; //我们规定网络通信的时候最大时候是1024字节
            while(true)
            {
                //收消息
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer); // 这里一定要写清楚,未来缓冲区的大小
                int n = recvfrom(sock_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); // 这里的-1为了兼容C向字符串的结尾加\0
                if(n > 0) buffer[n] = '\0'; //读取成功,设置\0
                else continue; //否则的话继续读取

                //拿到地址和端口,并且完成网络转主机化
                std::string clientip = inet_ntoa(peer.sin_addr);
                uint16_t clientport = ntohs(peer.sin_port); 

                std::cout <<clientip << "-"<< clientport << "#" << buffer << std::endl;

                //构建一个用户并检查
                std::string name = clientip;
                name += "-";
                name += std::to_string(clientport);

                //如果不存在,就插入,如果存在什么都不做
                addUser(name, peer);

                std::string message = name + ">> " + buffer;


                rq.push(message);

                // //做业务处理
                // std::string reponse = service_(buffer);
                
                // //发消息
                // sendto(sock_, reponse.c_str(), reponse.size() , 0, (struct sockaddr *)&peer, sizeof(peer));
            }
        }

        void Broadcast()
        {
            while (true)
            {

                std::string sendstring;
                rq.pop(&sendstring);

                std::vector<struct sockaddr_in> v; // 这里用vector优化一下效率,因为vector最终只在内存中拷贝数据
                {
                    LockGuard lockguard(&lock);
                    for (auto user : onlineuser)
                    {
                        v.push_back(user.second);
                    }
                }

                for (auto user : v)
                {
                    sendto(sock_, sendstring.c_str(), sendstring.size(), 0, (struct sockaddr *)&user, sizeof(user));
                }


            }
        }   


        ~UdpServer() 
        {
            pthread_mutex_destroy(&lock);

            c->join();
            p->join();

            delete c;
            delete p;
        }

    private:
        int sock_;
        uint16_t port_;
        //func_t service_; //
        std::unordered_map<std::string, struct sockaddr_in> onlineuser;
        RingQueue<std::string> rq;
        pthread_mutex_t lock;
        Thread *c;
        Thread *p;
        // std::string ip_;
    };

}
  • server.cc
#include<iostream>
#include<memory>
#include<string>

#include"udp_server.hpp"



using namespace std;
using namespace ns_server;


static void Usage(string proc)
{
    cout << "Usage:\n\t" << proc << "port\n" << endl;
}

//上层的业务处理, 不关心网络, 只负责业务处理
std::string transactionString(std::string requets)
{
    std::string result;
    char c;
    for(auto& r : requets)
    {
        if(islower(r))
        {
            c = toupper(r);
            result.push_back(c);
        }
        else
        result.push_back(r);
    }
    
    return result;
}

bool isPass(const std::string& command)
{   
    bool pass = true;

    auto pos = command.find("mv");
    if(pos != std::string::npos) pass = false;

    pos = command.find("rm");
    if(pos != std::string::npos) pass = false;

    // pos = command.find("");
    // if(pos != std::string::npos) pass = false;

    return pass;
}


std::string excuteCommend(const std::string& command)
{
    //1.安全检查
    if(!isPass(command)) return "Bad command!";

    //2.业务处理
    FILE* fp = popen(command.c_str(), "r");
    if(fp == nullptr) return "None"; 
    //3.获取结果
    char line[1024];
    std::string result;
    while(fgets(line, sizeof(line), fp) != NULL)
    {
        result += line;
    }

    pclose(fp);

    return result;
}

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

    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }
    
    //通过命令行参数获取端口号
    uint16_t port = atoi(argv[1]);
    
    // unique_ptr<UdpServer> server(new UdpServer("1.1.1.1", 8082));

    unique_ptr<UdpServer> server(new UdpServer(port));

    server->start();
    // server->Start();

    return 0;
}
  • client.hpp
#pragma once



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

#include"err.hpp"
  • client.cc
#include"udp_client.hpp"


#include<pthread.h>


using namespace std;


static void Usage(string proc)
{
    cout << "Usage:\n\t" << proc << "serverip  serverport  "<< "port\n" << endl;
}


void* reciver(void* args)
{
    int sock = *(static_cast<int *>(args)); // 获取到对应的套接字

    while(true)
    {
        // 收
        char buffer[2048];
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        int n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);
        if (n > 0) // 收到了数据
        {
            buffer[n] = '\0';
            cout << buffer << endl;
        }
    }

    return nullptr;
}



//./udp_client serverip serverport
int main(int argc, char* argv[])
{

    if(argc != 3)
    {
        Usage(argv[0]);
        exit(USAGE_ERR);
    }

    string serverip = argv[1];  //获取ip
    uint16_t serverport = atoi(argv[2]);  //获取port
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if(sock < 0)
    {
        std::cerr << "create socket error " << std::endl;
        exit(SOCKET_ERR);
    }

    //client 这里要不要bind呢?要的!
    //要不要自己bind呢?但是我们不需要自己bind,也不要自己bind,操作系统会帮我们bind。
    //为什么?因为我们要做到每个客户端的端口号不重复,也就是端口号的冲突问题,直接交给OS
    //那么server为什么要自己绑定?1.因为server的端口不能随意改变,众所周知且不能被改变的
    //同一家公司的port号,需要统一和规范的

    //明确server是谁
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server)); //将server内部的数据全初始化为0
    server.sin_family = AF_INET; //指定我们的网络模式为ipv4
    server.sin_port = htons(serverport); //将端口号主机转网络然后赋值给server
    server.sin_addr.s_addr = inet_addr(serverip.c_str()); //将字符串类型的ip转化为对应的用.分割的格式

    pthread_t tid;
    pthread_create(&tid, nullptr, &reciver, &sock);

    while(true)
    {
        //用户输入消息
        string message;
        std::cerr << "Please Enter Your Message#";
        //cin >> message;

        getline(cin, message);

        //什么时候bind? 在我们首次系统调用发送数据的时候,OS底层会随机选择clientport+自己的IP, 1.bind 2.构建发送数据的报文
        //发送给服务端
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));

    }


    pthread_join(tid, nullptr);

    return 0;
}

到这本篇博客的内容就到此结束了。
如果觉得本篇博客内容对你有所帮助的话,可以点赞,收藏,顺便关注一下!
如果文章内容有错误,欢迎在评论区指正

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/448786.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

鼠标在QTreeView、QTableView、QTableWidget项上移动,背景色改变

目录 1. 前言 2. 需求 3. 功能实现 3.1. 代码实现 3.2. 功能讲解 4. 附录 1. 前言 本博文用到了Qt的model/view framework框架,如果对Qt的“模型/视图/委托”框架不懂&#xff0c;本博文很难读懂。如果不懂这方面的知识&#xff0c;请在Qt Assistant 中输入Model/View…

一款适合程序员开发复杂系统的通用平台——JNPF 开发平台

在过去&#xff0c;很多开发工具更侧重代码编辑&#xff0c;针对数据库增删改查&#xff08;CRUD&#xff09;类的 Web 系统开发&#xff0c;在界面设计、前后端数据交互等环节主要还是靠写代码&#xff0c;效率比较低。目前很多所谓的低代码开发平台&#xff0c;大多数也都是基…

SQLServer数据库系列之:查询SQLServer数据库上面的连接信息、session信息、sql语句

SQLServer数据库系列之:查询SQLServer数据库上面的连接信息、session信息、sql语句 一、查询数据库上的连接信息二、查询SQLServer数据库的session信息SQLServer数据库从入门到精通系列文章之:SQLServer数据库百篇技术文章汇总 数据库专栏系列文章阅读传送门:详细整理汇总M…

超越 Siri 和 Alexa:探索LLM(大型语言模型)的世界

揭秘LLM&#xff1a;语言模型新革命&#xff0c;智能交互的未来趋势 近年来&#xff0c;虚拟助手的世界发生了重大转变。 虽然 Siri 和 Alexa 本身就是革命性的&#xff0c;但一种称为大型语言模型 (LLM) 的新型人工智能正在将虚拟助手的概念提升到一个全新的水平。 在这篇博文…

农业四情监测系统---气象科普

农业四情监测系统是一个集环境数据采集、分析与决策支持于一体的智能化系统。它主要通过对农田的土壤、气候、作物生长及病虫害等四个关键要素的实时监测&#xff0c;为农业生产提供精准的数据支持和科学的决策依据。 在土壤方面&#xff0c;系统能够检测土壤温度、湿度、pH值及…

我终于解决MathPage.wll文件找不到问题|(最新版Word上亲测)运行时错误,53’: 文件未找到:athPage.WLL

1、问题症状&#xff1a; 运行时错误&#xff0c;53’: 文件未找到:athPage.WLL 2、 解决方案 第一步 首先我们要先找到MathType安装目录下MathPage.wll文件&#xff0c;直接在此电脑中搜索MathPage.wll&#xff0c;找到文件所在位置。 第二步 打开Word文件&#xff0c…

App拉起微信小程序参考文章

App拉起微信小程序参考文章h5页面跳转小程序-----明文URL Scheme_weixin://dl/business/?appid*appid*&path*path*&qu-CSDN博客文章浏览阅读561次&#xff0c;点赞16次&#xff0c;收藏5次。仅需两步&#xff0c;就能实现h5跳转小程序&#xff0c;明文 URL Scheme&…

Linux---多线程(上)

一、线程概念 线程是比进程更加轻量化的一种执行流 / 线程是在进程内部执行的一种执行流线程是CPU调度的基本单位&#xff0c;进程是承担系统资源的基本实体 在说线程之前我们来回顾一下进程的创建过程&#xff0c;如下图 那么以进程为参考&#xff0c;我们该如何去设计创建一个…

闭包表(Closure Table)存储和查询树形数据结构

闭包表通过在关系表中记录树节点之间的直接和间接关系来表示节点之间的层次结构&#xff0c;目的是支持高效的树遍历和查询操作。 一、创建闭包表 CREATE TABLE departments (id int NOT NULL COMMENT ID,name varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_…

Redis冲冲冲——Redis持久化方式及其区别

目录 引出Redis持久化方式Redis入门1.Redis是什么&#xff1f;2.Redis里面存Java对象 Redis进阶1.雪崩/ 击穿 / 穿透2.Redis高可用-主从哨兵3.持久化RDB和AOF4.Redis未授权访问漏洞5.Redis里面安装BloomFilte Redis的应用1.验证码2.Redis高并发抢购3.缓存预热用户注册验证码4.R…

掌握React中的useCallback:优化性能的秘诀

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

XWPFDocument中XmlCursor的使用

类名&#xff1a; org.apache.xmlbeans Interface XmlCursor版本&#xff1a; 原xml代码&#xff1a; <w:p w14:paraId"143E3662" w14:textId"4167FBA7" w:rsidR"001506F2" w:rsidRPr"003F3D89" w:rsidRDefault"001506F2&qu…

OpenStack安装步骤

一、准备OpenStack安装环境 1、创建实验用的虚拟机实例。 内存建议16GB&#xff08;8GB也能运行&#xff09;CPU&#xff08;处理器&#xff09;双核且支持虚拟化硬盘容量不低于200GB&#xff08;&#xff01;&#xff09;网络用net桥接模式 运行虚拟机 2、禁用防火墙与SELin…

2024会声会影永久免费版新功能软件特色及新功能

会声会影2024永久免费版是一款收到很多用户公认的极佳视频编辑软件&#xff0c;里面的每一个功能都特别的强悍你能够一键给图片视频添加特效非常的过瘾&#xff0c;赶快来一起下载试试吧。 会声会影2023-安装包&#xff1a; https://souurl.cn/gtyDFc 会声会影2023-安装包&…

Golang 开发实战day03 - Arrays Slices

Golang 教程03 - Arrays&#xff0c;Slices Go语言中的数组和切片都是用于存储数据的类型&#xff0c;但它们之间存在一些重要的区别。了解这些区别对于有效地使用它们至关重要。 1. Arrays 数组 1.1 定义 数组是一种固定大小的数据结构&#xff0c;用于存储相同类型的值。…

web基础05-jQuery

目录 一、jQuery 1.概述 2.原生js与jQuery对比 3.特点 4.使用 &#xff08;1&#xff09;入口函数 &#xff08;2&#xff09;语法 &#xff08;3&#xff09;jQuery选择器 5.方法 &#xff08;1&#xff09;获取属性值&#xff1a; &#xff08;2&#xff09;删除属…

TCP三次握手,四次挥手状态转移过程

1.TCP状态转移过程 TCP连接的任意一端都是一个状态机,在TCP连接从建立到断开的整个过程中,连接两端的状态机将经历不同的状态变迁.理解TCP状态转移对于调试网络应用程序将有很大的帮助. 2.三次握手状态转换 3.四次挥手状态转换 4.TIME WAIT状态详解 为什么要有一个"TIME…

【网络安全】-数字证书

数字证书 数字证书是互联网通讯中用于标志通讯各方身份信息的一串数字或数据&#xff0c;它为网络应用提供了一种验证通信实体身份的方式。具体来说&#xff0c;数字证书是由权威的证书授权&#xff08;CA&#xff09;中心签发的&#xff0c;包含公开密钥拥有者信息以及公开密…

c# 调用ip2region组件 根据ip地址进行定位归属地运营商

需求描述&#xff1a;当项目中需要将IP转换成对应的归属地以及运营商&#xff0c;那么通过ip2region组件即可完美实现。 p2region本身支持net4.5以上&#xff0c;还有个ip2region.net组件&#xff0c;它要求net6及以上。所以&#xff0c;根据自己项目的需求即可选择其中一种方…

【SpringCloud微服务实战03】Nacos 注册中心

一、Nacos安装 官方文档安装Nacos教程:Nacos 快速开始 这里安装的是1.4.7版本,安装之后访问http://127.0.0.1:8848/nacos 管理界面如下:(用户名:nacos,密码:nacos) 二、Nacos服务注册和发现 1、在父工程中配置文件pom.xml 中添加spring-cloud-alilbaba的管理依赖:…