C/C++ Linux网络编程2 - Socket编程与简单UDP服务器客户端

上篇文章:Linux网络编程 - 1网络编程基础-CSDN博客

代码仓库:橘子真甜 (yzc-YZC) - Gitee.com

目录

[一. Socket编程API与结构体](#一. Socket编程API与结构体)

[1.1 Socketaddr结构体](#1.1 Socketaddr结构体)

[二. UDP网络编程API⭐](#二. UDP网络编程API⭐)

[2.1 socket](#2.1 socket)

[2.2 bind](#2.2 bind)

[2.3 sendto](#2.3 sendto)

[2.4 recvfrom](#2.4 recvfrom)

[三. UDP服务端代码](#三. UDP服务端代码)

[3.1 udpserver.hpp](#3.1 udpserver.hpp)

[3.2 udpserver.cc](#3.2 udpserver.cc)

[四. UDP客户端代码](#四. UDP客户端代码)

[4.1 udpclient.hpp](#4.1 udpclient.hpp)

[4.2 udpclient.cc](#4.2 udpclient.cc)

[五. 测试](#五. 测试)


一. Socket编程API与结构体

socket是插座的意思,插就是应用程序,座就是网络socket。通信双方都通过socket进行通信。

1.1 Socketaddr结构体

有三套结构体,sockaddr sockaddr_in sockaddr_un

sockaddr:原始套接字,可以通过传输层控制底层结构,常用于网络中的监听,抓包等功能。结构如下:

sockaddr_in:网络套接字,常用于跨主机之间的网络通信。内部**集合了主机的IP和端口,在大部分网络编程API中,需要强制转化为原始套接字。**结构如下:

Unix域间套接字:用于主机内部的通信,类似于管道和共享内存。结构如下:

在使用这些结构体的时候,通过将 sockaddr_in sockaddr_un转化为 sockaddr即可实现相应类型的通信。

其实就是一种多态的思想,soackaddr就是基类,剩下两种是派生类。

二. UDP网络编程API⭐

UDP的特点是不保证可靠传输,面向数据报,无连接。

所以:

我们每一次接收数据的时候,应该将一个完整的报文读取完毕。

由于无连接,发送数据的时候需要带上自己的ip和端口等信息

收数据的时候也要定义对象来获取对方的ip和端口等信息

2.1 socket

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

//函数原型,用于创建一个用于网络通信的套接字fd
int socket(int domin, int type, int protocol)


//参数说明
//domin : 域,表示是需要网络通信还是主机内部通信

//type  : 通信的类型
//SOCK_STREAM    是流式套接字通信,即TCP通信
//SOCK_DGRAM     是报文套接字通信,即UDP通信

// protocol,
// 分为tcpprotocol udpprotoclo 但是我们一般设置为0,因为type参数选择之后,这个就会自动固定

//返回值,返回一个用于通信的文件fd,其实socket与open是非常类似的

Linux中一切皆文件,打开网络与打开普通文件是一样的。读写网络数据与读写文件数据也是非常类似的

2.2 bind

用于绑定sockfd与IP端口

cpp 复制代码
#include <sys/types.h>
#inckude <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
sockfd : 使用socket获取的文件描述符
addr : 传入的结构体,该结构体是你需要通信的方式,内部包含了IP和端口等信息
addrlen : 传入结构体的长度,需要注意类型

返回值:绑定成功返回0,失败返回-1

//注意,我们定义下面三种结构体的时候需要带上头文件#include <arpa/inet.h>
struct sockaddr s;
s.sin_family = AF_INET    表示协议家族,我们需要传入协议,如AF_INET表示使用网络通信
s.sin_port                表示绑定端口号
s.sin_addr                存储了IP号码
s.sin_addr.s_addr         即可获取IP地址

2.3 sendto

这个接口常用于UDP服务器/客户端之间的发送信息

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>

用于发送信息到网络中
//函数原型
ssize_t sendto(int sockfd, void *buf, size_t len, int flags, struct sockaddr* dest_addr, socklen_t *addrlen);
//将  len长度的buf数据通过sockfd这个套接字发送到网络中
flags为0表示阻塞

//输入形参数
dest_addr 对方接收数据的IP地址和端口
addrlen   对方接收数据结构体长度

2.4 recvfrom

用于UDP通信时候接收数据

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     接收信息的文件描述符
bur        接收信息写入的缓冲区    len 接收信息的长度
flags      接收信息的方式,一般默认设置为0    表示阻塞方式,没有信息阻塞等待

//下面两个参数是输入输出形参数,是为了获取对方的IP信息与端口号码,协议方式
src_addr    对方IP的信息
addrlen     该信息的长度,注意这个参数在定义的时候不需要初始化为src_addr的长度 addrlen = sizeof(src_addr)

三. UDP服务端代码

这个服务器用于接收客户端的代码,然后打印输出。

3.1 udpserver.hpp

服务器的代码框架如下:

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

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

static const std::string defaultIp = "0.0.0.0";
class udpServer
{
public:
    udpServer(uint16_t port, const std::string &ip = defaultIp)
        : _sockfd(-1), _ip(ip), _port(port) {}

    void init() {}

    void run() {}

private:
    int _sockfd; // 套接字

    std::string _ip; // 服务器ip
    uint16_t _port;  // 绑定的端口
};

需要变量来存储服务器的 通信fd,ip和端口。

init用于初始化服务器:包含socket,bind

run:用于服务器的运行,一个服务器一般是一个死循环执行代码的。

一般来说,一台主机是有很多ip的,为了能够保证接收数据不丢失,服务器绑定的IP一般都是"0.0.0.0"表示服务器可以接收底层任意ip地址的网络数据

init代码:

cpp 复制代码
  void init()
        {
            // 1.socket创建套接字,udp需要使用 报文
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
            if (_sockfd < 0)
            {
                perror("socket");
                exit(errno);
            }
            printf("socket success\n");

            // 2.绑定fd和端口
            // 首先要设置好通信结构体
            struct sockaddr_in serveraddr;
            memset(&serveraddr, 0, sizeof(serveraddr));
            serveraddr.sin_family = AF_INET;
            serveraddr.sin_addr.s_addr = inet_addr(_ip.c_str()); // 需要转化为大端数据
            serveraddr.sin_port = htons(_port);                  // 需要转化为大端数据
            socklen_t len = sizeof(serveraddr);                  // 设置bind第三个参数

            int n = bind(_sockfd, (struct sockaddr *)&serveraddr, len);
            if (n < 0)
            {
                perror("bind");
                exit(errno);
            }
            printf("bind success\n");
            // UDP在绑定好之后,就能运行了
        }

run:

服务器运行的时间是很长的,所以我们需要使用一个死循环来读取数据。

cpp 复制代码
        void run()
        {
            char buffer[1024];
            while (true)
            {
                // 收数据,要定义对象获取对方的ip和端口
                struct sockaddr_in clientaddr;
                memset(&clientaddr, 0, sizeof(clientaddr));
                socklen_t clientaddrlen = sizeof(clientaddr);

                // recvfrom接收数据
                int count = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&clientaddr, &clientaddrlen);

                if (count > 0)
                {
                    // 通过设置好的sockaddr获取ip,端口等信息
                    std::string clientip = inet_ntoa(clientaddr.sin_addr);
                    uint16_t clientport = ntohs(clientaddr.sin_port);
                    // 打印收到数据的信息
                    printf("client[%s][%d] --> server:%s\n", clientip.c_str(), clientport, buffer);
                }
                // 清空 buffer
                memset(buffer, 0, sizeof(buffer));
            }
        }

总代码如下:

cpp 复制代码
namespace YZC
{
    static const std::string defaultIp = "0.0.0.0";
    class udpServer
    {
    public:
        udpServer(uint16_t port, const std::string &ip = defaultIp)
            : _sockfd(-1), _ip(ip), _port(port) {}

  void init()
        {
            // 1.socket创建套接字,udp需要使用 报文
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
            if (_sockfd < 0)
            {
                perror("socket");
                exit(errno);
            }
            printf("socket success\n");

            // 2.绑定fd和端口
            // 首先要设置好通信结构体
            struct sockaddr_in serveraddr;
            memset(&serveraddr, 0, sizeof(serveraddr));
            serveraddr.sin_family = AF_INET;
            serveraddr.sin_addr.s_addr = inet_addr(_ip.c_str()); // 需要转化为大端数据
            serveraddr.sin_port = htons(_port);                  // 需要转化为大端数据
            socklen_t len = sizeof(serveraddr);                  // 设置bind第三个参数

            int n = bind(_sockfd, (struct sockaddr *)&serveraddr, len);
            if (n < 0)
            {
                perror("bind");
                exit(errno);
            }
            printf("bind success\n");
            // UDP在绑定好之后,就能运行了
        }

        void run()
        {
            char buffer[1024];
            while (true)
            {
                // 收数据,要定义对象获取对方的ip和端口
                struct sockaddr_in clientaddr;
                memset(&clientaddr, 0, sizeof(clientaddr));
                socklen_t clientaddrlen = sizeof(clientaddr);

                // recvfrom接收数据
                int count = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&clientaddr, &clientaddrlen);

                if (count > 0)
                {
                    // 通过设置好的sockaddr获取ip,端口等信息
                    std::string clientip = inet_ntoa(clientaddr.sin_addr);
                    uint16_t clientport = ntohs(clientaddr.sin_port);
                    // 打印收到数据的信息
                    printf("client[%s][%d] --> server:%s\n", clientip.c_str(), clientport, buffer);
                }
                // 清空 buffer
                memset(buffer, 0, sizeof(buffer));
            }
        }

    private:
        int _sockfd; // 套接字

        std::string _ip; // 服务器ip
        uint16_t _port;  // 绑定的端口
    };
}

3.2 udpserver.cc

这个代码包含main函数,用于服务器的启动和测试

cpp 复制代码
#include <iostream>
#include <memory>
#include "udpserver.hpp"

// 设置使用方法
void Usage(const char *s)
{
    printf("Usage:\r\n%s serverport\n", s);
}

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

    // 使用智能指针管理服务器,可以自动销毁。防止内存泄漏
    std::unique_ptr<YZC::udpServer> usptr(new YZC::udpServer(atoi(argv[1])));
    usptr->init();
    usptr->run();
    return 0;
}

四. UDP客户端代码

这个客户端用于向服务端发送数据

思考一下:服务端为何需要bind绑定fd和端口?

因为,如果没有指明端口,其他客户端如何连接服务器?

思考一下:客户端需要显示绑定fd和端口吗?

不需要,因为通信是由客户端发起的,服务端被动接收。此时服务端无需知道客户端具体的ip和端口信息

服务端可以直接通过recvfrom获取我的ip/端口等信息

4.1 udpclient.hpp

框架与server类似

cpp 复制代码
namespace YZC
{
    class udpClient
    {
    public:
        udpClient(std::string &ip, uint16_t port)
            : _ip(ip), _port(port) {}

        void init()
        {
        }

        void start()
        {
        }

    private:
        int _sockfd; // 监听套接字

        std::string _ip; // 目的ip
        uint16_t _port;  // 目的端口
    };
}

init : 上面讨论过了,只需要socket,无需显示bind。如果没有bind,在发送数据的时候内核会自动帮我们找到没有使用的port进行bind

cpp 复制代码
        void init()
        {
            // 1.socket创建套接字,udp需要使用 报文
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
            if (_sockfd < 0)
            {
                perror("socket");
                exit(errno);
            }
            printf("client socket success\n");

            // bind由OS自行决定
        }

start:

用于发送数据

cpp 复制代码
  void start()
        {
            // 发送数据,需要指定对方的ip和端口
            struct sockaddr_in serveraddr;
            memset(&serveraddr, 0, sizeof(serveraddr));

            // 设置ip/端口/通信等信息。注意转化为大端数据
            serveraddr.sin_family = AF_INET;
            serveraddr.sin_addr.s_addr = inet_addr(_ip.c_str());
            serveraddr.sin_port = htons(_port);

            socklen_t clientaddrlen = sizeof(serveraddr);

            // 可以发送数据
            char buffer[1024];
            while (true)
            {
                memset(buffer, 0, sizeof(buffer));
                std::cout << "请输入你想要向server发送的数据;";
                std::cin >> buffer;
                int count = sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&serveraddr, clientaddrlen);
                if (count <= 0)
                {
                    printf("数据发送异常,client quit\n");
                    close(_sockfd);
                    exit(-1);
                }
                printf("数据发送成功\n");
            }
        }

总代码如下:

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

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#include <cstring>

namespace YZC
{
    class udpClient
    {
    public:
        udpClient(std::string &ip, uint16_t port)
            : _ip(ip), _port(port) {}

        void init()
        {
            // 1.socket创建套接字,udp需要使用 报文
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
            if (_sockfd < 0)
            {
                perror("socket");
                exit(errno);
            }
            printf("client socket success\n");

            // bind由OS自行决定
        }

  void start()
        {
            // 发送数据,需要指定对方的ip和端口
            struct sockaddr_in serveraddr;
            memset(&serveraddr, 0, sizeof(serveraddr));

            // 设置ip/端口/通信等信息。注意转化为大端数据
            serveraddr.sin_family = AF_INET;
            serveraddr.sin_addr.s_addr = inet_addr(_ip.c_str());
            serveraddr.sin_port = htons(_port);

            socklen_t clientaddrlen = sizeof(serveraddr);

            // 可以发送数据
            char buffer[1024];
            while (true)
            {
                memset(buffer, 0, sizeof(buffer));
                std::cout << "请输入你想要向server发送的数据;";
                std::cin >> buffer;
                int count = sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&serveraddr, clientaddrlen);
                if (count <= 0)
                {
                    printf("数据发送异常,client quit\n");
                    close(_sockfd);
                    exit(-1);
                }
                printf("数据发送成功\n");
            }
        }

    private:
        int _sockfd; // 监听套接字

        std::string _ip; // ip
        uint16_t _port;  // 绑定的端口
    };
}

4.2 udpclient.cc

cpp 复制代码
#include <iostream>
#include <memory>
#include "udpclient.hpp"

// 设置使用方法
void Usage(const char *s)
{
    printf("Usage:\r\n%s serverip serverport\n", s);
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(-1);
    }

    // 使用智能指针管理服务器,可以自动销毁。防止内存泄漏
    std::unique_ptr<YZC::udpClient> ucptr(new YZC::udpClient(argv[1], atoi(argv[2])));
    ucptr->init();
    ucptr->start();
    return 0;
}

五. 测试

使用上面的代码来测试服务端server与客户端client 能否通信

这里首先了解一下本地回环地址 127.0.0.1。使用命令 ifconfig查看网卡信息

下面的数据是我在自己的云服务器输入ifconfig获取的。

cpp 复制代码
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.52.126  netmask 255.255.192.0  broadcast 172.17.63.255
        inet6 fe80::216:3eff:fe0a:5e1e  prefixlen 64  scopeid 0x20<link>
        ether 00:16:3e:0a:5e:1e  txqueuelen 1000  (Ethernet)
        RX packets 1944787  bytes 1196104294 (1.1 GiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1537251  bytes 634615558 (605.2 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 746248  bytes 164466741 (156.8 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 746248  bytes 164466741 (156.8 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

可以看第二段的第二行,本地回环地址是 127.0.0.1

首先要启动服务器

然后启动客户端

运行结果如下:

可以看到,我们的数据被客户端正常发送了。服务端也接收到来正确的数据

这样一来就能保证 server - client之间的通信,当然这份代码是比较简单的。

相关推荐
咬_咬2 小时前
C++仿muduo库高并发服务器项目:Poller模块
服务器·开发语言·c++·epoll·muduo
深圳市恒讯科技2 小时前
服务器与普通个人电脑的主要区别是什么?
运维·服务器
qq_281317472 小时前
nginx安装配置、故障处置、性能优化
运维·nginx
未来之窗软件服务2 小时前
服务器运维(十一)SQLite3 php封装——东方仙盟炼气期
运维·服务器·sqlite·服务器运维·数据库驱动·东方仙盟
QT 小鲜肉3 小时前
【QT/C++】Qt样式设置之CSS知识(系统性概括)
linux·开发语言·css·c++·笔记·qt
yachuan_qiao3 小时前
专业的建筑设备监控管理系统选哪家
大数据·运维·python
Elias不吃糖3 小时前
NebulaChat 框架学习笔记:深入理解 Reactor 与多线程同步机制
linux·c++·笔记·多线程
洋哥网络科技3 小时前
centos 7.9搭建安装confluence7
linux·centos·知识图谱
占疏3 小时前
访问日志查询功能
java·服务器·flask