网络套接字1

news2024/10/5 9:14:03

网络套接字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.coloradmin.cn/o/1507385.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JVM3_数据库连接池虚引用ConnectionFinalizerPhantomReference引起的FullGC压力问题排查

背景 XOP服务运行期间&#xff0c;查看Grafana面板&#xff0c;发现堆内存周期性堆积&#xff0c;观察FullGC的时间&#xff0c;xxx&#xff0c;需要调查下原因 目录 垃圾收集器概述 常见的垃圾收集器分区收集策略为什么CMS没成为默认收集器 查看JVM运行时环境分析快照 Pha…

msfconsole中db_namp的使用方法以及如何让msf连接数据库

一、db_nmap使用方法 1.打开数据库 1.1查看数据库postgresql连接状态 systemctl status postgresql查看数据库postgresql连接状态、 1.2启动postgresql systemctl start postgresql启动postgresql 1.3初始化 msfdb init初始化 2.C段扫描(db_nmap的使用) 2.1 db_nmap -sP 192…

AIGC实战——GPT(Generative Pre-trained Transformer)

AIGC实战——GPT 0. 前言1. GPT 简介2. 葡萄酒评论数据集3. 注意力机制3.1 查询、键和值3.2 多头注意力3.3 因果掩码 4. Transformer4.1 Transformer 块4.2 位置编码 5. 训练GPT6. GPT 分析6.1 生成文本6.2 注意力分数 小结系列链接 0. 前言 注意力机制能够用于构建先进的文本…

windows和linux系统安装redis

Redis安装 Redis安装与启动windows服务 Redis 安装 这样安装完在系统服务中并没有redis服务 redis服务启动 Redis安装与启动Linux服务 1.下载压缩包到服务器 我下载的是最新版本7.0.12&#xff0c;这里我是直接下载到了root目录下 wget https://github.com/redis/redis…

ChatGPT逐步进入留学圈但并不能解决留学规划的问题

2022 年底&#xff0c;一个能像人类一样对话的AI软件ChatGPT&#xff0c;在5天内突破一百万用户&#xff0c;风靡全球&#xff0c;如今用户已达1.8亿。 四个月后&#xff0c;ChatGPT进化为GPT4版本。该版本逻辑、数学推理能力卓越。拿留美标准化考试举例&#xff0c;GPT4能够在…

图论练习6

[NOIP2013]车站分级 Here 解题思路 由于起始点之间所选的站号&#xff0c;相互之间一定满足那么对于起始点间未选择的站号&#xff0c;一定满足选择的站号考虑用边来维护信息&#xff0c;表示的级别大于按题意&#xff0c;则车站会被分为几个联通块&#xff0c;且保证块内无环…

使用Java和PostGis的全国A级风景区数据入库实战

目录 前言 一、数据介绍 1、空间数据 2、属性表说明 3、QGIS数据预览 二、PostGIS空间数据库设计 1、空间表结构 三、Java空间入库 1、实体定义 2、数据操作Mapper 3、业务层实现 4、入库 5、数据入库验证 总结 前言 星垂平野阔&#xff0c;月涌大江流”“晴川历历…

WinoGrande数据集分享

来源: AINLPer公众号&#xff08;每日干货分享&#xff01;&#xff01;&#xff09; 编辑: ShuYini 校稿: ShuYini 时间: 2024-3-11 该数据集由华盛顿大学的研究人员提出&#xff0c;它是一个大规模的常识推理挑战数据集&#xff0c;包含约44,000个问题&#xff0c;旨在评估和…

【实战项目】网络编程:在Linux环境下基于opencv和socket的人脸识别系统--C++实现

&#x1f31e;前言 这里我们会实现一个项目&#xff1a;在linux操作系统下基于OpenCV和Socket的人脸识别系统。 目录 &#x1f31e;前言 &#x1f31e;一、项目介绍 &#x1f31e;二、项目分工 &#x1f31e;三、项目难题 &#x1f31e;四、实现细节 &#x1f33c;4.1 关…

【APP逆向】酒仙网预约茅台程序,包含逆向过程详解

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 所属的专栏:爬虫实战,零基础、进阶教学 景天的主页:景天科技苑 文章目录 酒仙网预约抢购茅台1.抓包分析,账户名和密码登录2.短信登录3.登录+茅台预约 密码登录酒仙网预约抢购茅台 目标:账号登…

重启 explorer 进程的正确做法(二)

重启资源管理器进程的方法不唯一&#xff0c;但长期以来大家对实施方法用的不到位。 在上一篇中我认为&#xff1a;“我们往往使用 TerminateProcess 并传入 PID 和特殊结束代码 1 或者 taskkill /f /im 等方法重启资源管理器( explorer.exe )&#xff0c;其实这是不正确的。我…

jdk17出现错误无法初始化主类 和NoClassDefFoundError:Vector的解决方法

概述&#xff1a;网上流传文章大多都是编译和运行都加下面这串代码 --add-modulesjdk.incubator.vector我估计他们大多都是复制粘贴的文章&#xff0c;这种东西就是电子垃圾&#xff0c;在idea中&#xff0c;大多人都习惯用maven来构建java项目&#xff0c;接下来我将讲解使用…

Android14音频进阶:AudioTrack如何巧妙衔接AudioFlinger(五十七)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏:多媒体系统工程师系列【原创干货持续更新中……】🚀 人生格言: 人生从来没有捷径,只…

设计模式十:原型模式

文章目录 1、原型模式1.1 类创建过程1.2 浅拷贝1.3 深拷贝 2、示例2.1 简单形式2.2 复杂形式 3、spring中的原型模式3.1 ArrayList的原型模式3.2 spring中的原型模式 1、原型模式 原型模式就是从一个对象再创建另外一个可定制的对象&#xff0c; 而且不需要知道任何创建的细节。…

前端解决跨域问题( 6种方法 )

本专栏是汇集了一些HTML常常被遗忘的知识&#xff0c;这里算是温故而知新&#xff0c;往往这些零碎的知识点&#xff0c;在你开发中能起到炸惊效果。我们每个人都没有过目不忘&#xff0c;过久不忘的本事&#xff0c;就让这一点点知识慢慢渗透你的脑海。 本专栏的风格是力求简洁…

使用JDBC操作数据库

意志、工作和等待是成功的金字塔的基石。 Will, work and wait are the pyramidal cornerstones for success. 文章目录 JDBC简介&#xff1a;JDBC访问数据库步骤StatementPreparedStatement JDBC简介&#xff1a; 在Java应用程序中&#xff0c;JDBC&#xff08;Java Database…

CSS 入门指南(二)CSS 常用样式及注册页面案例

CSS 常用样式 颜色属性 常见样式的颜色属性&#xff1a; color&#xff1a;定义文本的颜色border-color&#xff1a;定义边框的颜色background-color&#xff1a;设置背景色 颜色属性值设置方式&#xff1a; 十六进制值 - 如&#xff1a;&#xff03;FF0000一个RGB值 - 如…

docker安装ollama

拉取镜像 docker pull ollama/ollama 运行容器 &#xff08;挂载路径 D:\ollama 改成你自己喜欢的路径&#xff09; CPU only docker run -d -v D:\ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama Nvidia GPU&#xff08;没试过这个&#xff09; doc…

LeetCode203:移除链表元素

题目描述 给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 解题思想 使用虚拟头节点 代码 struct ListNode {int val;ListNode* next;ListNode() :val(0), next(nullptr) {};ListNode(i…

1 Tomcat服务器Servlet入门

今日目标 web知识概述tomcat【重点】创建servlet xml anno&#xff08;注解&#xff09;servlet执行原理servlet生命周期servlet体系结构 1.web相关知识概述【了解】 1.WEB简介 学习目标 了解什么是web 内容讲解 Web&#xff08;World Wide Web&#xff09;即全球广域网…