网络编程套接字(2): 简单的UDP网络程序

文章目录

  • [网络编程套接字(2): 简单的UDP网络程序](#网络编程套接字(2): 简单的UDP网络程序)
    • [3. 简单的UDP网络程序](#3. 简单的UDP网络程序)
      • [3.1 服务端创建](#3.1 服务端创建)
        • [(1) 创建套接字](#(1) 创建套接字)
        • [(2) 绑定端口号](#(2) 绑定端口号)
        • [(3) sockaddr_in结构体](#(3) sockaddr_in结构体)
        • [(4) 数据的接收与发送](#(4) 数据的接收与发送)
      • [3.2 客户端创建](#3.2 客户端创建)
      • [3.3 代码编写](#3.3 代码编写)
        • [(1) v1_简单发送消息](#(1) v1_简单发送消息)
        • [(2) v2_小写转大写](#(2) v2_小写转大写)
        • [(3) v3_模拟命令行解释器](#(3) v3_模拟命令行解释器)
        • [(4) v4_多线程版本的群聊系统](#(4) v4_多线程版本的群聊系统)
        • [(5) v5_Windows与Linux配合聊天室](#(5) v5_Windows与Linux配合聊天室)

网络编程套接字(2): 简单的UDP网络程序

3. 简单的UDP网络程序

3.1 服务端创建

(1) 创建套接字

create an endpoint for communication: 创建用于通信的端点

cpp 复制代码
头文件:
        #include <sys/types.h>         
        #include <sys/socket.h>

函数原型:
        int socket(int domain, int type, int protocol);

参数说明:
		第一个参数domain:   指定套接字的通信域
        第二个参数type:     指定套接字的服务类型(套接字的种类) 
        第三个参数protocol: 代表创建套接字的协议(默认为0), 给0,系统会自动判断是tcp还是udp

返回值: 
	    套接字创建成功: 返回一个文件描述符
        套接字创建失败: 返回-1, 并且设置错误码

关于socket参数详细介绍:

(1) domain: 指定套接字的通信域,相当于 struct sockaddr结构体的前16比特位(2字节)

domain的选项是以宏的形式给出的,我们直接选用即可。常用就是上面框住的两个:

  • AF_UNIX,本地通信
  • AF_INET (IPv4)或者 AF_INET6(IPv6),网络通信

(2) type: 指定套接字的服务类型

该参数的选项也是像domain一样以宏的形式给出,直接选用。常用的是上面两个:

  • SOCK_STREAM: 基于TCP的网络通信,流式套接字,提供的是流式服务(对应TCP的特点:面向字节流)

  • SOCK_DGRAM: 基于UDP的网络通信,套接字数据报,提供的用户数据报服务(对应UDP的特点:面向数据报)

(2) 绑定端口号

bind a name to socket:将名称绑定到套接字

cpp 复制代码
头文件:
       #include <sys/types.h>          
       #include <sys/socket.h>

函数原型:
       int bind(int sockfd, const struct sockaddr *addr,  socklen_t addrlen);

参数说明:
	   第一个参数sockfd:  文件描述符, 即要绑定的套接字
	   第二个参数addr:    网络相关的结构体, 包含IP地址、端口号等
	   第三个参数addrlen: 传入结构体addr(第二个参数)的实际长度大小
           
返回值:
	   绑定成功: 返回0
	   绑定失败: 返回-1,并且设置错误码

参数addr的类型是:struct sockaddr *,也就是如图的结构体

我们需要做的就是:定义一个 sockaddr_in 的结构体,即上图的第二个结构体,然后对该结构体进行内容填充,填完就把给结构体传给第二个参数addr,需要强制类型转换

(3) sockaddr_in结构体

  • __SOCKADDR_COMMON是一个宏
cpp 复制代码
#define	__SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family

sa_prefix:代表外面传入的参数sin_

sa_prefix##family:##代表合并拼接,意思是sa_prefix与family合并拼接

sa_prefix就是sin_,则sa_prefix##family表示sin_family

sa_family_t:代表16位整数

就是这16位地址类型

  • sin_port: 是当前服务器需要绑定的端口号,它的类型是 in_port_t,代表16位的整数
  • sin_addr: 代表IP地址,它的类型是一个in_addr的结构体,它里面的内容是32位的整数
  1. 关于这个IP地址:我们要传入字符串风格的,但是这里需要4字节整数风格,所以需要转化,比如"1.1.1.1"-> uint32_t,问:能不能强转呢?
    不能强转, 强转只能改变类型, 不改变二进制构成

  2. 我们转化完了还是本主机的4字节序列,需要网络序列,所以要将主机序列转化成为网络序列

上面的2步用 inet_addr函数就可以完成

  1. 但是我们的云服务器,或者一款服务器,一般不要指明某一个确定的IP

所以这里的ip地址我们填 INADDR_ANY,这是一个宏,代表 0.0.0.0,叫做任意地址绑定

  • sin_zero: 表示该结构体的填充字段(即上面讲的sin_family,sin_port,sin_addr.s_add)

总结: 未来使用这个函数时,需要所以填充:sin_family,sin_port,sin_addr.s_addr这3个字段,因为不关注其他字段,所以在填充之前需要对该结构体清空,我们可以采用 memset或 bzero函数来完成。

bind的作用:

上面如果我们只设置了sockaddr_in这个结构体,它只是在用户空间的特定函数栈帧上,不在内核中,所以bind的作用就是把文件字段进行绑定关联,这样这个文件就是网络文件

(4) 数据的接收与发送

接收

receive a message from a socket:从套接字接收消息

cpp 复制代码
头文件: 
        #include <sys/types.h>
        #include <sys/socket.h>

函数原型:
		 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr 							*src_addr, socklen_t *addrlen);

参数说明:
	     第一个参数sockfd: 文件描述符,就是上面我们绑定好的套接字
    	 第二个参数buf: 接收数据的缓冲区(自己定义)
         第三参数len:   缓冲区的长度
         第四个参数flags: 读取方式,默认设为0表示阻塞式读取
         第五个参数src_addr: (输入)对应套接字的接收缓冲区
         第六个参数addrlen:(输出)src_addr结构体的长度

返回值:
         成功: 返回实际读到的字节数
         失败: 失败返回-1,并设置错误码

socklen_t 是一个32位的无符号整数

  • 参数src_addr与addrlen:输入输出型参数

  • src_addr: 输入时传入对应套接字的接收缓冲区,输出时包含客户端的ip和port

  • addrlen: 输入时传入对应套接字的接收缓冲区,输出时表示实际输出的结构体大小

我们做的是定义一个 sockaddr_in 的结构体,把结构体传给参数src_addr,需要强制类型转换

发送

send a message on a socket: 在套接字上发送消息

cpp 复制代码
头文件:
		   #include <sys/types.h>
           #include <sys/socket.h>

函数原型:
		   ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                           const struct sockaddr *dest_addr, socklen_t addrlen);
参数说明:
			
           第一个参数sockfd: 文件描述符,从哪个套接字去发送消息
           第二个参数buf: 发送数据的缓冲区
           第三参数len: 要发送的数据长度
           第四个参数flags:发送方式,默认设为0表示阻塞式发送
           第五个参数dest_addr:下面解释
           第六个参数addrlen:dest_addr结构体的长度

返回值:
		   成功: 实际发送的字节数
		   失败: 失败返回-1,并设置错误码
  • dest_addr和addrlen 是一个输入型参数

  • dest_addr:指向目的地址的结构体指针,表示要发给谁

  • addrlen:表示目的地址结构体的长度

我们做的是定义一个 sockaddr_in 的结构体,然后对该结构体进行内容填充,填完就把给结构体传给dest_addr**,需要强制类型转换**

3.2 客户端创建

还是3步:创建套接字,bind(不需要自己绑定,由OS自动分配),处理数据接收与发送

3.3 代码编写

这里一共提供5个版本的udp代码

err.hpp:这个代码是公用的后续不在给出

cpp 复制代码
#pragma once
enum
{
    USAGE_ERR=1,
    SOCKET_ERR,
    BIND_ERR,
};

(1) v1_简单发送消息

客户端向服务端发送消息,服务端收到后再把消息发回给客户端

udp_server.hpp

cpp 复制代码
#include<iostream>
#include<memory>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include"err.hpp"
using namespace std;


namespace ns_server
{

    const static uint16_t default_port=8080;

    class Udpserver
    {
    public:
        Udpserver(uint16_t port=default_port)
            :port_(port)
        {
            cout<<"server addr: "<<port_<<endl;
        }

        void InitServer()
        {
            // 1. 创建socket接口, 打开网络文件(本质)
            sock_=socket(AF_INET,SOCK_DGRAM,0);
            if(sock_<0)
            {
                cerr<<"create socket error: "<<strerror(errno)<<endl;
                exit(SOCKET_ERR);
            }
            cout<<"create socket success: "<<sock_<<endl;   // sock_ = 3

            // 2. 给服务器指明IP地址和Port端口号

            // 填充一下服务器的IP和Port
            struct sockaddr_in local;     // 里面有很多字段  local是在用户空间的特定函数栈帧上,不在内核中!
            
            // 清空local
            bzero(&local,sizeof(local));  // 用memset也可以

            // 填充sockaddr_in结构
            local.sin_family=AF_INET;
            local.sin_port=htons(port_);         // 端口号要出现在网络中, 主机序列转网络序列

            // 使用 inet_addr就可以做下面两件事情:
            // (1) 字符串风格的IP地址,转换成为4字节int  --- 不能强转, 强转只能改变类型, 不改变二进制构成
            // (2) 需要将主机序列转化成为网络序列

            // (3)云服务器,或者一款服务器,一般不要指明某一个确定的IP
            local.sin_addr.s_addr=INADDR_ANY;   // 让我们的udp_server在启动的时候, bind本主机上的任意IP    

            // 把套接字字段和文件字段进行关联  --- 网络文件
            int n=bind(sock_,(struct sockaddr*)&local,sizeof(local));    
            if(n<0)
            {
                cerr<<"bind socket error: "<<strerror(errno)<<endl;
                exit(BIND_ERR);
            }
            cout<<"bind socket success: "<<sock_<<endl;   

        }

        void Start()
        {
            char buffer[1024];       // 保存用户数据的缓冲区
            while(true)
            {
                // 收到来自客户端发送的消息

                struct sockaddr_in peer;          // 远端
                socklen_t len = sizeof(peer);     // 这里一定要写清楚, 未来你传入的缓冲区大小
                // 假设消息是字符串, -1是为缓冲区预留一个空间,方便添加'\0'
                int n = recvfrom(sock_,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,                                  &len);  
                
                if(n>0)    // 读取数据成功
                    buffer[n]='\0';
                else
                    continue;

                // 提取client信息   --- debug
                string clientip=inet_ntoa(peer.sin_addr); // 把4字节对应的IP转化成字符串风格的
                uint16_t clientport=ntohs(peer.sin_port);  // 网络序列转主机序列
                cout<< clientip << "-" <<clientport<< "# "<<buffer<<endl;

                // 把消息发给别人

                // 网络套接字本质是文件, 往文件中写入时\0并不需要写到文件中, \0是C语言的规定
                // 谁给我发的, 我就把消息转给谁
                // peer结构体字段是从网络中拿的, 本来就是网络序列, 直接发就行
                sendto(sock_,buffer,strlen(buffer),0,(struct sockaddr*)&peer, 		  	                        sizeof(peer));
            }
        }

        ~Udpserver()
        {}

    private:
        int sock_;         //套接字(文件描述符)
        uint16_t port_;    //端口号(本地主机序列构建的port)
    };
}

udp_server.cc

cpp 复制代码
#include"udp_server.hpp"
using namespace ns_server;


// 运行格式: ./udp_server port

// 使用手册
static void usage(string proc)
{
    cout<<"usage:\n\t"<<proc<<" port\n"<<endl;
}

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

    uint16_t port=atoi(argv[1]);         //命令行参数转换成uint16_t类型

    unique_ptr<Udpserver> usvr(new Udpserver(port));

    usvr->InitServer();     // 服务器初始化

    usvr->Start();

    return 0;
}

udp_client.cc

cpp 复制代码
#pragma once

#include<iostream>
#include"err.hpp"
#include<sys/types.h>
#include<sys/socket.h>
#include<cstring>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;

// 127.0.0.1 本地环回, 就表示当前的主机, 通常用来进行本地环回通信或者测试

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

// udp_client serverip serverport
int main(int argc,char*argv[])
{
    if(argc !=3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }

    // 拿到服务端的ip和端口号
    string serverip=argv[1];
    uint16_t serverport=atoi(argv[2]);

    // 1. 创建套接字
    int sock=socket(AF_INET,SOCK_DGRAM,0);
    if (sock < 0)
    {
        cerr << "create socket error: " << strerror(errno) << endl;
        exit(SOCKET_ERR);
    }

    // 2. 关于客户端的绑定
    // client这里要不要bind呢? 要的 socket通信的本质 [clienttip: clientport, serverip:        serverport]
    // 要不要自己bind呢? 不需要自己bind, 也不要自己bind, OS自动给我们进行bind --- 为什么?
    // client的port要随机让OS分配防止client出现启动冲突 
    // server的端口不能随意改变, 众所周知且不能随意改变的, 同一家公司的port号需要统一规范化

    // 明确服务器是谁
    struct sockaddr_in server;
    memset(&server,0,sizeof(server));
    server.sin_family=AF_INET;
    server.sin_port=htons(serverport);     //主机序列转网络序列
    server.sin_addr.s_addr=inet_addr(serverip.c_str());

    // 3. 向服务器发送消息(这里暂时由用户充当)
    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)-1,0,(struct sockaddr*)&temp,&len);
        if(n>0)
        {
            buffer[n]='\0';
            cout<<"server echo# "<<buffer<<endl;
        }
    }
}

运行结果:

先运行服务端,再启动客户端,客户端先用本地环回进行测试,测试成功

运行程序后看到套接字是创建成功的,对应得到到的文件描述符是3,这也很好理解,因为0、1、2默认被标准输入流、标准输出流和标准错误流占用了,此时最小的、未被使用用的文件描述符就是3

(2) v2_小写转大写

v2在v1版本的基础增加了业务处理,上层使用了回调函数实现大小写转换

udp_server.hpp

cpp 复制代码
#pragma once

#include<iostream>
#include<memory>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include<functional>
#include"err.hpp"
using namespace std;


namespace ns_server
{

    const static uint16_t default_port=8080;
    using func_t =function<string(string)>;    //这是一个函数

    class Udpserver
    {
    public:
        Udpserver(func_t cb, uint16_t port=default_port)
            :service_(cb)
            ,port_(port)
        {
            cout<<"server addr: "<<port_<<endl;
        }

        void InitServer()
        {
            // 1. 创建socket接口, 打开网络文件(本质)
            sock_=socket(AF_INET,SOCK_DGRAM,0);
            if(sock_<0)
            {
                cerr<<"create socket error: "<<strerror(errno)<<endl;
                exit(SOCKET_ERR);
            }
            cout<<"create socket success: "<<sock_<<endl;   // sock_ = 3

            // 2. 给服务器指明IP地址和Port端口号

            // 填充一下服务器的IP和Port
            struct sockaddr_in local;     // 里面有很多字段  local是在用户空间的特定函数栈帧上,不在内核中!
            
            // 清空local
            bzero(&local,sizeof(local));  // 用memset也可以

            // 填充sockaddr_in结构
            local.sin_family=AF_INET;
            local.sin_port=htons(port_);         // 端口号要出现在网络中, 主机序列转网络序列

            // 使用 inet_addr就可以做下面两件事情:
            // (1) 字符串风格的IP地址,转换成为4字节int  --- 不能强转, 强转只能改变类型, 不改变二进制构成
            // (2) 需要将主机序列转化成为网络序列

            // (3)云服务器,或者一款服务器,一般不要指明某一个确定的IP
            local.sin_addr.s_addr=INADDR_ANY;   // 让我们的udp_server在启动的时候, bind本主机上的任意IP    

            // 把套接字字段和文件字段进行关联  --- 网络文件
            int n=bind(sock_,(struct sockaddr*)&local,sizeof(local));    
            if(n<0)
            {
                cerr<<"bind socket error: "<<strerror(errno)<<endl;
                exit(BIND_ERR);
            }
            cout<<"bind socket success: "<<sock_<<endl;   

        }

        void Start()
        {
            char buffer[1024];       // 保存用户数据的缓冲区
            while(true)
            {
                // 收到来自客户端发送的消息

                struct sockaddr_in peer;                 // 远端
                socklen_t len = sizeof(peer);             // 这里一定要写清楚, 未来你传入的缓冲区大小
                // 假设消息是字符串, -1是为缓冲区预留一个空间,方便添加'\0'
                int n = recvfrom(sock_,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,                                  &len);  
                
                if(n>0)    // 读取数据成功
                    buffer[n]='\0';
                else
                    continue;

                // 提取client信息   --- debug
                string clientip=inet_ntoa(peer.sin_addr); // 把4字节对应的IP转化成字符串风格的
                uint16_t clientport=ntohs(peer.sin_port);  // 网络序列转主机序列
                cout<< clientip << "-" <<clientport<< "# "<<buffer<<endl;


                // 做业务处理
                string response=service_(buffer);

                // 把消息发给别人

                // 网络套接字本质是文件, 往文件中写入时\0并不需要写到文件中, \0是C语言的规定
                // 谁给我发的, 我就把消息转给谁
                // peer结构体字段是从网络中拿的, 本来就是网络序列, 直接发就行
                sendto(sock_,response.c_str(),response.size(),0,(struct sockaddr*)&peer, 						sizeof(peer));
            }
        }

        ~Udpserver()
        {}

    private:
        int sock_;         //套接字(文件描述符)
        uint16_t port_;    //端口号(本地主机序列构建的port)
        func_t service_;    //我们的网络服务器刚刚解决的是网络IO的问题, 要进行业务处理(一个类内的回调方法)
    };
}

udp_server.cc

cpp 复制代码
#include"udp_server.hpp"
using namespace ns_server;


// 运行格式: ./udp_server port

// 使用手册
static void usage(string proc)
{
    cout<<"usage:\n\t"<<proc<<" port\n"<<endl;
}

// 上层的业务处理, 不关心网络发送, 只负责信息处理即可

// 这里是小写转大写
string transactionString(string request)   // request就是一个字符串
{
    string ret;
    char c;
    for(auto&r:request)
    {
        if(islower(r))
        {
            c=toupper(r);
            ret.push_back(c);
        }
        else
        {
            ret.push_back(r);
        }
    }
    return ret;
}
int main(int argc,char*argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }

    uint16_t port=atoi(argv[1]);         //命令行参数转换成uint16_t类型

    unique_ptr<Udpserver> usvr(new Udpserver(transactionString,port));

    usvr->InitServer();     // 服务器初始化

    usvr->Start();

    return 0;
}

udp_client.cc

cpp 复制代码
#pragma once

#include<iostream>
#include"err.hpp"
#include<sys/types.h>
#include<sys/socket.h>
#include<cstring>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;

// 127.0.0.1 本地环回, 就表示当前的主机, 通常用来进行本地环回通信或者测试

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

// udp_client serverip serverport
int main(int argc,char*argv[])
{
    if(argc !=3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }

    // 拿到服务端的ip和端口号
    string serverip=argv[1];
    uint16_t serverport=atoi(argv[2]);

    // 1. 创建套接字
    int sock=socket(AF_INET,SOCK_DGRAM,0);
    if (sock < 0)
    {
        cerr << "create socket error: " << strerror(errno) << endl;
        exit(SOCKET_ERR);
    }

    // 2. 关于客户端的绑定
    // client这里要不要bind呢? 要的 socket通信的本质 [clienttip: clientport, serverip:        		serverport]
    // 要不要自己bind呢? 不需要自己bind, 也不要自己bind, OS自动给我们进行bind --- 为什么?
    // client的port要随机让OS分配防止client出现启动冲突 
    // server的端口不能随意改变, 众所周知且不能随意改变的, 同一家公司的port号需要统一规范化

    // 明确服务器是谁
    struct sockaddr_in server;
    memset(&server,0,sizeof(server));
    server.sin_family=AF_INET;
    server.sin_port=htons(serverport);     //主机序列转网络序列
    server.sin_addr.s_addr=inet_addr(serverip.c_str());

    // 3. 向服务器发送消息(这里暂时由用户充当)
    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)-1,0,(struct sockaddr*)&temp,&len);
        if(n>0)
        {
            buffer[n]='\0';
            cout<<"server echo# "<<buffer<<endl;
        }
    }
}

运行结果:

(3) v3_模拟命令行解释器

v3是在v2原有的业务处理下修改了功能,只要我们在客户端输入命令服务端就会返回运行结果,popen函数可以实现简单的命令行解释

udp_server.hpp

cpp 复制代码
#pragma once

#include<iostream>
#include<memory>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include<functional>
#include"err.hpp"
using namespace std;

namespace ns_server
{
    const static uint16_t default_port=8080;
    using func_t =function<string(string)>;    //这是一个函数

    class Udpserver
    {
    public:
        Udpserver(func_t cb, uint16_t port=default_port)
            :service_(cb)
            ,port_(port)
        {
            cout<<"server addr: "<<port_<<endl;
        }

        void InitServer()
        {
            // 1. 创建socket接口, 打开网络文件(本质)
            sock_=socket(AF_INET,SOCK_DGRAM,0);
            if(sock_<0)
            {
                cerr<<"create socket error: "<<strerror(errno)<<endl;
                exit(SOCKET_ERR);
            }
            cout<<"create socket success: "<<sock_<<endl;   // sock_ = 3

            // 2. 给服务器指明IP地址和Port端口号

            // 填充一下服务器的IP和Port
            struct sockaddr_in local;     // 里面有很多字段  local是在用户空间的特定函数栈帧上,不在内核中!
            
            // 清空local
            bzero(&local,sizeof(local));  // 用memset也可以

            // 填充sockaddr_in结构
            local.sin_family=AF_INET;
            local.sin_port=htons(port_);         // 端口号要出现在网络中, 主机序列转网络序列

            // 使用 inet_addr就可以做下面两件事情:
            // (1) 字符串风格的IP地址,转换成为4字节int  --- 不能强转, 强转只能改变类型, 不改变二进制构成
            // (2) 需要将主机序列转化成为网络序列

            // (3)云服务器,或者一款服务器,一般不要指明某一个确定的IP
            local.sin_addr.s_addr=INADDR_ANY;   // 让我们的udp_server在启动的时候, bind本主机上的任意IP    

            // 把套接字字段和文件字段进行关联  --- 网络文件
            int n=bind(sock_,(struct sockaddr*)&local,sizeof(local));    
            if(n<0)
            {
                cerr<<"bind socket error: "<<strerror(errno)<<endl;
                exit(BIND_ERR);
            }
            cout<<"bind socket success: "<<sock_<<endl;   

        }


        void Start()
        {
            char buffer[1024];       // 保存用户数据的缓冲区
            while(true)
            {
                // 收到来自客户端发送的消息

                struct sockaddr_in peer;                 // 远端
                socklen_t len = sizeof(peer);             // 这里一定要写清楚, 未来你传入的缓冲区大小
                // 假设消息是字符串, -1是为缓冲区预留一个空间,方便添加'\0'
                int n = recvfrom(sock_,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,                                  &len);  
                
                if(n>0)    // 读取数据成功
                    buffer[n]='\0';
                else
                    continue;

                // 提取client信息   --- debug
                string clientip=inet_ntoa(peer.sin_addr); // 把4字节对应的IP转化成字符串风格的
                uint16_t clientport=ntohs(peer.sin_port);  // 网络序列转主机序列
                cout<< clientip << "-" <<clientport<< "# "<<buffer<<endl;

                // 做业务处理
                string response=service_(buffer);

                // 把消息发给别人

                // 网络套接字本质是文件, 往文件中写入时\0并不需要写到文件中, \0是C语言的规定
                // 谁给我发的, 我就把消息转给谁
                // peer结构体字段是从网络中拿的, 本来就是网络序列, 直接发就行
                sendto(sock_,response.c_str(),response.size(),0,(struct sockaddr*)&peer, 						sizeof(peer));
            }
        }

        ~Udpserver()
        {}

    private:
        int sock_;         //套接字(文件描述符)
        uint16_t port_;    //端口号(本地主机序列构建的port)
        func_t service_;    //我们的网络服务器刚刚解决的是网络IO的问题, 要进行业务处理(一个类内的回调方法)
    };
}

udp_server.cc

cpp 复制代码
#include"udp_server.hpp"
using namespace ns_server;


// 运行格式: ./udp_server port

// 使用手册
static void usage(string proc)
{
    cout<<"usage:\n\t"<<proc<<" port\n"<<endl;
}

// 上层的业务处理, 不关心网络发送, 只负责信息处理即可
static bool isPass(string &command)
{
    auto pos=command.find("rm");
    if(pos!=string::npos) return false;
    pos=command.find("mv");
    if(pos!=string::npos) return false;
    pos=command.find("while");
    if(pos!=string::npos) return false;
    pos=command.find("kill");
    if(pos!=string::npos) return false;

    return true;
}

// 让同学们, 在你的本地把命令给我, server再把结果给你!
string excuteCommand(string command)   // command就是一个命令
{
    // 1. 安全检查
    if(!isPass(command))
        return "you are a bad man";

    // 2. 业务逻辑处理
    FILE*fp=popen(command.c_str(),"r");
    if(fp==nullptr)
        return "None";

    // 3. 获取结果
    char line[1024];
    string ret;
    while(fgets(line,sizeof(line),fp)!=NULL)
    {
        ret+=line;
    }
    pclose(fp);

    return ret;
}

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

    uint16_t port=atoi(argv[1]);         //命令行参数转换成uint16_t类型

    unique_ptr<Udpserver> usvr(new Udpserver(excuteCommand,port));

    usvr->InitServer();     // 服务器初始化

    usvr->Start();

    return 0;
}

udp_server.cc

cpp 复制代码
#pragma once

#include<iostream>
#include"err.hpp"
#include<sys/types.h>
#include<sys/socket.h>
#include<cstring>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;


// 127.0.0.1 本地环回, 就表示当前的主机, 通常用来进行本地环回通信或者测试

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

// udp_client serverip serverport
int main(int argc,char*argv[])
{
    if(argc !=3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }

    // 拿到服务端的ip和端口号
    string serverip=argv[1];
    uint16_t serverport=atoi(argv[2]);

    // 1. 创建套接字
    int sock=socket(AF_INET,SOCK_DGRAM,0);
    if (sock < 0)
    {
        cerr << "create socket error: " << strerror(errno) << endl;
        exit(SOCKET_ERR);
    }

    // 2. 关于客户端的绑定
    // client这里要不要bind呢? 要的 socket通信的本质 [clienttip: clientport, serverip: serverport]
    // 要不要自己bind呢? 不需要自己bind, 也不要自己bind, OS自动给我们进行bind --- 为什么?
    // client的port要随机让OS分配防止client出现启动冲突 
    // server的端口不能随意改变, 众所周知且不能随意改变的, 同一家公司的port号需要统一规范化

    // 明确服务器是谁
    struct sockaddr_in server;
    memset(&server,0,sizeof(server));
    server.sin_family=AF_INET;
    server.sin_port=htons(serverport);     //主机序列转网络序列
    server.sin_addr.s_addr=inet_addr(serverip.c_str());

    // 3. 向服务器发送消息(这里暂时由用户充当)
    while(true)
    {
        // 用户输入
        string message;
        cout<< "[遇健的服务器]# ";
        getline(cin,message);
    
        // 什么时候bind呢?
        // 在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP, (1)bind (2)构建            发送的数据报文

        // 发送
        sendto(sock,message.c_str(),message.size(), 0, (struct sockaddr*)&server,                       sizeof(server));

        // 把消息再收回来(回显回来)

        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<<"server echo# "<<buffer<<endl;
        }

    }
}

运行结果:

(4) v4_多线程版本的群聊系统

v4在v3的基础上加入了之前写的生产消费者模型,多线程实现了群聊系统

LockGuard.hpp

cpp 复制代码
#include<iostream>
#include<pthread.h>
using namespace std;

class Mutex   //自己不维护锁,由外部传入
{
public:
    Mutex(pthread_mutex_t* mutex)
        :_pmutex(mutex)
    {

    }

    void lock()
    {
        pthread_mutex_lock(_pmutex);
    }

    void unlock()
    {
        pthread_mutex_unlock(_pmutex);
    }

    ~Mutex()
    {}

private:
    pthread_mutex_t* _pmutex;   //锁的指针
};

class LockGuard  //自己不维护锁,由外部传入
{
public:
    LockGuard(pthread_mutex_t* mutex)
        :_mutex(mutex)
    {
        _mutex.lock();
    }

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

private:
    Mutex _mutex;   //锁的指针
};

RingQueue.hpp

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<vector>
#include<time.h>
#include<sys/types.h>
#include<unistd.h>
#include<semaphore.h>
#include<mutex>
using namespace std;

// 生产者和消费者要有自己的下标来表征生产和消费要访问哪个资源
static const int N=5;

template<class T>
class RingQueue
{
private:

    void P(sem_t &s)
    {
        sem_wait(&s);
    }

    void V(sem_t &s)
    {
        sem_post(&s);
    }

    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)
        :_ring(num)
        ,_cap(num)
    {
        sem_init(&_data_sem,0,0);
        sem_init(&_space_sem,0,num);
        _c_step=_p_step=0;

        pthread_mutex_init(&_c_mutex,nullptr);
        pthread_mutex_init(&_p_mutex,nullptr);
    }

    void push(const T&in)     // 对应生产者
    {
        // 1.信号量的好处:
        // 可以不用在临界区内部做判断, 就可以知道临界资源的使用情况

        // 2.什么时候用锁, 什么时候用sem? --- 你对应的临界资源, 是否被整体使用!

        // 生产 --- 先要申请信号量
        // 信号量申请成功 - 则一定能访问临界资源
        P(_space_sem);
        Lock(_p_mutex);
        // 一定要有对应的空间资源给我!不用做判断, 是哪一个资源给生产者呢
        _ring[_p_step++]=in;
        _p_step%=_cap;
        V(_data_sem);
        Unlock(_p_mutex);
    }

    void pop(T*out)           // 对应消费者
    {
        // 消费
        P(_data_sem);    // 1.   先申请信号量是为了更高效
        Lock(_c_mutex);  // 2. 
        *out=_ring[_c_step++];
        _c_step%=_cap;
        V(_space_sem);
        Unlock(_c_mutex);
    }

    ~RingQueue()
    {
        sem_destroy(&_data_sem);
        sem_destroy(&_space_sem);

        pthread_mutex_destroy(&_c_mutex);
        pthread_mutex_destroy(&_p_mutex);
    }
private:
    vector<T> _ring;
    int _cap;           // 环形队列容器大小
    sem_t _data_sem;    // 只有消费者关心
    sem_t _space_sem;   // 只有生产者关心
    int _c_step;        // 消费位置
    int _p_step;        // 生产位置

    pthread_mutex_t _c_mutex;
    pthread_mutex_t _p_mutex;
};

udp_server.hpp

cpp 复制代码
#include<iostream>
#include<memory>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include<functional>
#include"err.hpp"
#include<unordered_map>
#include"ringQueue1.hpp"
#include"lockGuard.hpp"
#include"thread.hpp"
using namespace std;


// 群聊系统 --- 一个线程收消息, 一个线程发消息
// 标识一个客户端: ip+port , 使用unordered_map构建<ip+port, 客户端套接字>来表示某个用户发的消息

namespace ns_server
{

    const static uint16_t default_port=8080;
    using func_t =function<string(string)>;    //这是一个函数

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

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

        void StartServer()
        {
            // 1. 创建socket接口, 打开网络文件(本质)
            sock_=socket(AF_INET,SOCK_DGRAM,0);
            if(sock_<0)
            {
                cerr<<"create socket error: "<<strerror(errno)<<endl;
                exit(SOCKET_ERR);
            }
            cout<<"create socket success: "<<sock_<<endl;   // sock_ = 3

            // 2. 给服务器指明IP地址和Port端口号

            // 填充一下服务器的IP和Port
            struct sockaddr_in local;     // 里面有很多字段  local是在用户空间的特定函数栈帧上,不在内核中!
            
            // 清空local
            bzero(&local,sizeof(local));  // 用memset也可以

            // 填充sockaddr_in结构
            local.sin_family=AF_INET;
            local.sin_port=htons(port_);         // 端口号要出现在网络中, 主机序列转网络序列

            // 使用 inet_addr就可以做下面两件事情:
            // (1) 字符串风格的IP地址,转换成为4字节int  --- 不能强转, 强转只能改变类型, 不改变二进制构成
            // (2) 需要将主机序列转化成为网络序列

            // (3)云服务器,或者一款服务器,一般不要指明某一个确定的IP
            local.sin_addr.s_addr=INADDR_ANY;   // 让我们的udp_server在启动的时候, bind本主机上的任意IP    

            // 把套接字字段和文件字段进行关联  --- 网络文件
            int n=bind(sock_,(struct sockaddr*)&local,sizeof(local));    
            if(n<0)
            {
                cerr<<"bind socket error: "<<strerror(errno)<<endl;
                exit(BIND_ERR);
            }
            cout<<"bind socket success: "<<sock_<<endl;   

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

        void addUser(const string&name,const struct sockaddr_in&peer)
        {
            // online[name]=peer
            LockGuard lockguard(&_lock);
            auto iter=onlineuser.find(name);
            if(iter!=onlineuser.end())        // 存在(找到了)直接返回
                return;

            onlineuser.insert(make_pair(name,peer));   // 不存在(没找到)就插入
        }

        void Recv()
        {
            char buffer[1024];       // 保存用户数据的缓冲区
            while(true)
            {
                // 收到来自客户端发送的消息

                struct sockaddr_in peer;                 // 远端
                socklen_t len = sizeof(peer);             // 这里一定要写清楚, 未来你传入的缓冲区大小
                // 假设消息是字符串, -1是为缓冲区预留一个空间,方便添加'\0'
                int n = recvfrom(sock_,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,                                  &len);  
                
                if(n>0)    // 读取数据成功
                    buffer[n]='\0';
                else
                    continue;

                // 提取client信息   --- debug
                string clientip=inet_ntoa(peer.sin_addr); // 把4字节对应的IP转化成字符串风格的
                uint16_t clientport=ntohs(peer.sin_port);  // 网络序列转主机序列
                cout<< clientip << "-" <<clientport<< "# "<<buffer<<endl;


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

                 // 构建哈希表来存储用户 - 如果不存在,就插入;如果存在,什么都不做
                addUser(name,peer);  

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

                _rq.push(message);    // 消息放入环形队列中
            }
        }


        // 发消息  --- 给所有在线用户
        void Broadcast()
        {
            while(true)
            {
                string sendstring;
                _rq.pop(&sendstring);    // 从环形队列中读到了消息   

                vector<struct sockaddr_in> v;   // 把需要发送的信息放到(拷贝)一个数组中<这是内存级的拷贝>
                {
                    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));
                    cout<<"send done ..."<<sendstring<<endl;
                }
            }
        }

        ~Udpserver()
        {
            pthread_mutex_destroy(&_lock);
            p->join();
            c->join();

            delete p;
            delete c;
        }

    private:
        int sock_;         //套接字(文件描述符)
        uint16_t port_;    //端口号(本地主机序列构建的port)
        unordered_map<string, struct sockaddr_in> onlineuser;   // 保存在线用户 --- 需要加锁保证安全
        pthread_mutex_t _lock;
        RingQueue<string> _rq;
        Thread*p;
        Thread*c;
    };
}

udp_server.cc

cpp 复制代码
#include"udp_server.hpp"
using namespace ns_server;


// 运行格式: ./udp_server port

// 使用手册
static void usage(string proc)
{
    cout<<"usage:\n\t"<<proc<<" port\n"<<endl;
}

// 上层的业务处理, 不关心网络发送, 只负责信息处理即可

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

    uint16_t port=atoi(argv[1]);         //命令行参数转换成uint16_t类型

    unique_ptr<Udpserver> usvr(new Udpserver(port));

    usvr->StartServer();

    return 0;
}

udp_client.cc

cpp 复制代码
#pragma once

#include<iostream>
#include"err.hpp"
#include<sys/types.h>
#include<sys/socket.h>
#include<cstring>
#include<string>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;

// 127.0.0.1 本地环回, 就表示当前的主机, 通常用来进行本地环回通信或者测试

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


// udp_client serverip serverport
int main(int argc,char*argv[])
{
    if(argc !=3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }

    // 拿到服务端的ip和端口号
    string serverip=argv[1];
    uint16_t serverport=atoi(argv[2]);

    // 1. 创建套接字
    int sock=socket(AF_INET,SOCK_DGRAM,0);
    if (sock < 0)
    {
        cerr << "create socket error: " << strerror(errno) << endl;
        exit(SOCKET_ERR);
    }

    // 2. 关于客户端的绑定
    // client这里要不要bind呢? 要的 socket通信的本质 [clienttip: clientport, serverip: serverport]
    // 要不要自己bind呢? 不需要自己bind, 也不要自己bind, OS自动给我们进行bind --- 为什么?
    // client的port要随机让OS分配防止client出现启动冲突 
    // server的端口不能随意改变, 众所周知且不能随意改变的, 同一家公司的port号需要统一规范化

    // 明确服务器是谁
    struct sockaddr_in server;
    memset(&server,0,sizeof(server));
    server.sin_family=AF_INET;
    server.sin_port=htons(serverport);     //主机序列转网络序列
    server.sin_addr.s_addr=inet_addr(serverip.c_str());

    // 3. 向服务器发送消息(这里暂时由用户充当)
    while(true)
    {
        // 用户输入
        string message;
        cout<< "[遇健的服务器]# ";
        getline(cin,message);
    
        // 什么时候bind呢?
        // 在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP, (1)bind (2)构建发送的数据报文

        // 发送
        sendto(sock,message.c_str(),message.size(), 0, (struct sockaddr*)&server,                        sizeof(server));

        // 把消息再收回来(回显回来)

        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<<"server echo# "<<buffer<<endl;
        }

    }
}

运行结果:

(5) v5_Windows与Linux配合聊天室

我们可以以Linux云服务器作为服务端,Windows作为客户端,在Windows下我们要修改成Windows下的接口,同时开放云服务器的端口号,使用v4版本的服务端代码

Windows下的客户端

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS

#include<iostream>
#include<WinSock2.h>
#include<string>
#include<cstring>
using namespace std;

#pragma warning(disable:4996)
#pragma comment(lib,"ws2_32.lib")

uint16_t serverport = 8080;
std::string serverip = "47.108.235.67";

//std::string serverip = "127.0.0.1";

int main()
{
    WSADATA WSAData;

    if (WSAStartup(MAKEWORD(2, 2), &WSAData) != 0)
    {
        cerr << "init error" << endl;
        return -1;
    }

    SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);

    if (sock < 0)
    {
        cerr << "create socket error: " << strerror(errno) << endl;
        exit(-2);
    }

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);     //主机序列转网络序列
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
   
    // 3. 向服务器发送消息(这里暂时由用户充当)
    while (true)
    {
        // 用户输入
        string message;
        cout << "Please Enter Your Message# ";
        getline(cin, message);

        // 发送
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server,                      sizeof(server));

        char buffer[2048];
        struct sockaddr_in temp;
        int 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;     // 往1号文件描述符输出
        }
    }
    closesocket(sock);
    WSACleanup();

    return 0;
}

运行结果:

相关推荐
Mogu_cloud5 分钟前
pcdn盒子连接方式
网络·智能路由器
Hqst_Kevin7 分钟前
Hqst 品牌 H81801D 千兆 DIP 网络变压器在光猫收发器机顶盒中的应用
运维·服务器·网络·5g·网络安全·信息与通信·信号处理
Hqst 网络变压器 Andy8 分钟前
交换机最常用的网络变压器分为DIP和SM
网络·依赖倒置原则
DREAM依旧17 分钟前
《深入了解 Linux 操作系统》
linux
网安康sir25 分钟前
2024年三个月自学手册 网络安全(黑客技术)
网络·安全·web安全
Nigoridl32 分钟前
MSF的使用学习
网络·web安全
阿赭ochre41 分钟前
Linux环境变量&&进程地址空间
linux·服务器
Iceberg_wWzZ42 分钟前
数据结构(Day14)
linux·c语言·数据结构·算法
可儿·四系桜1 小时前
如何在多台Linux虚拟机上安装和配置Zookeeper集群
linux·服务器·zookeeper
Flying_Fish_roe1 小时前
linux-软件包管理-包管理工具(Debian 系)
linux·运维·debian