【Linux】socket编程1-服务端代码

在这里插入图片描述

欢迎来到Cefler的博客😁
🕌博客主页:折纸花满衣
🏠个人专栏:题目解析

在这里插入图片描述


👉🏻socket网络编程常用头文件

在C++中使用socket,你需要包含以下头文件:

  1. <sys/socket.h>:这个头文件包含了使用socket所需的系统级别的函数和数据结构的声明。

  2. <netinet/in.h>:该头文件包含了定义网络地址结构 struct sockaddr_in 的声明,以及一些网络相关的常量和函数原型。

  3. <arpa/inet.h>:这个头文件包含了一些与网络地址转换相关的函数原型,例如 inet_addr()inet_ntoa()

  4. <unistd.h>:这个头文件包含了一些系统调用函数的原型,例如 close(),exit()

在 Windows 平台上,你可能需要包含不同的头文件,例如 <winsock2.h><ws2tcpip.h>

👉🏻socket网络编程预备知识函数

bzero

bzero 函数在标准C库中已经被弃用,它通常用于将一块内存区域清零。在现代C++编程中,推荐使用更安全和更具可移植性的替代方案,如 memset 函数或者使用 C++ 标准库提供的 std::fill 函数。

然而,如果你仍然需要了解 bzero 函数的工作原理,我可以简要介绍一下。

bzero 函数通常的形式是这样的:

void bzero(void *s, size_t n);

它接受两个参数:

  • s:指向要清零的内存区域的指针。
  • n:要清零的字节数。

bzero 函数的作用是将从 s 开始的连续 n 个字节设置为零。它通常用于初始化或清空某些内存区域,例如清空一个字符数组或一个结构体。

以下是一个示例用法:

#include <stdio.h>
#include <strings.h>

int main() {
    char buffer[10];
    bzero(buffer, sizeof(buffer));  // 将 buffer 中的所有字节清零

    // 在清零后,buffer 中的内容应该全是 0
    for (int i = 0; i < sizeof(buffer); ++i) {
        printf("%d ", buffer[i]);
    }

    return 0;
}

然而,需要注意的是,bzero 函数已经被标记为废弃,因为它不够安全。在一些情况下,编译器可能会发出警告,建议使用更安全的替代方案。

inet_addr

inet_addr 函数是一个用于将点分十进制的 IPv4 地址转换为网络字节顺序的 32 位二进制整数的函数。它在网络编程中经常用于将 IP 地址转换为网络套接字地址结构中使用的格式。

该函数的原型如下:

#include <arpa/inet.h>

in_addr_t inet_addr(const char *cp);

它接受一个指向以空字符结尾的字符串的指针作为参数,并返回一个 in_addr_t 类型的值,该值表示转换后的 IP 地址。

下面是一个示例用法:

#include <stdio.h>
#include <arpa/inet.h>

int main() {
    const char *ip_address_str = "192.168.1.1";
    in_addr_t ip_address = inet_addr(ip_address_str);

    if (ip_address == INADDR_NONE) {
        printf("Invalid IP address\n");
    } else {
        printf("IP address in network byte order: %u\n", ip_address);
    }

    return 0;
}

需要注意的是,inet_addr 函数存在一些限制和问题,例如不能处理 IPv4 子网掩码,而且在处理不合法的 IP 地址时可能会返回 INADDR_NONE。因此,更安全和更强大的替代方案是使用 inet_pton 函数,它能够处理更多的情况并提供更好的错误处理机制。

recvfrom

recvfrom 函数是在进行网络编程时常用的一个函数,它用于从一个已连接的套接字接收数据,或者从未连接的套接字接收数据和地址。具体来说,recvfrom 通常用于在 UDP 套接字中接收数据,因为 UDP 是面向消息的协议,不像 TCP 那样有连接的概念。

该函数的原型如下:

#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:一个指向 sockaddr 结构的指针,用于存储发送方的地址信息。
  • addrlen:一个指向 socklen_t 类型变量的指针,用于存储发送方地址信息的长度。

recvfrom 函数的返回值是接收到的数据的字节数。如果返回值为 -1,则表示出现了错误。

下面是一个简单的示例用法,演示了如何使用 recvfrom 函数接收 UDP 数据:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    char buffer[BUFFER_SIZE];
    struct sockaddr_in servaddr, cliaddr;
    socklen_t len = sizeof(cliaddr);

    // 创建UDP套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 绑定地址和端口
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(PORT);

    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }

    // 接收数据
    ssize_t n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, 0,
                         (struct sockaddr *)&cliaddr, &len);
    buffer[n] = '\0'; // 在接收到的数据后面添加字符串结束符

    printf("Received message: %s\n", buffer);

    close(sockfd);
    return 0;
}

这个示例创建了一个UDP套接字,并绑定到指定的端口。然后使用 recvfrom 函数接收来自客户端的数据,并将数据打印出来。

ssize_t 是一个有符号整数类型,用于表示某些系统调用(如 readwriterecvfrom 等)的返回值或错误码。它通常被定义为 typedef,在标准库中可以找到类似以下的定义:

typedef long ssize_t;

ssize_t 的长度通常与指针的长度相同,即在 32 位系统上为 4 字节,在 64 位系统上为 8 字节。因为它是有符号类型,所以可以表示负数,通常用于表示字节数或者错误码,以便能够返回错误的情况。

在标准的 POSIX 环境中,ssize_t 的定义在 sys/types.h 头文件中。

sendto

sendto 函数用于通过已连接或未连接的套接字发送数据到指定的目标地址。通常在网络编程中,它被用于在 UDP 套接字中发送数据,因为 UDP 是面向消息的协议,不像 TCP 那样有连接的概念。

该函数的原型如下:

#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:一个指向 sockaddr 结构的指针,用于指定目标地址。
  • addrlen:目标地址信息的长度。

sendto 函数的返回值是实际发送的数据的字节数。如果返回值为 -1,则表示出现了错误。

下面是一个简单的示例用法,演示了如何使用 sendto 函数发送 UDP 数据:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    char buffer[BUFFER_SIZE];
    struct sockaddr_in servaddr;

    // 创建UDP套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 设置服务器地址和端口
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(PORT);

    // 发送数据
    const char *message = "Hello, UDP Server!";
    ssize_t n = sendto(sockfd, message, strlen(message), 0,
                       (const struct sockaddr *)&servaddr, sizeof(servaddr));
    if (n < 0) {
        perror("sendto failed");
        exit(EXIT_FAILURE);
    }

    printf("Message sent successfully.\n");

    close(sockfd);
    return 0;
}

这个示例创建了一个UDP套接字,并设置了服务器的地址和端口。然后使用 sendto 函数向服务器发送数据。

👉🏻正式开搞——编写服务端代码

Makefile(生成目标文件)

Udpserver:Main.cc
	g++ -o $@ $^ -std=c++14
.PHONY:clean
clean:
	rm -rf Udpserver

Log.hpp(用来打印日志信息)

#pragma once

#include <iostream>
#include <fstream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

using namespace std;
enum
{
    Debug = 0,
    Info,
    Warning,
    Error,
    Fatal
};

enum
{
    Screen = 10,
    OneFile,
    ClassFile
};

std::string LevelToString(int level)
{
    switch (level)
    {
    case Debug:
        return "Debug";
    case Info:
        return "Info";
    case Warning:
        return "Warning";
    case Error:
        return "Error";
    case Fatal:
        return "Fatal";
    default:
        return "Unknown";
    }
}

const int defaultstyle = Screen;
const std::string default_filename = "log.";
const std::string logdir = "log";

class Log
{
public:
    Log() : style(defaultstyle), filename(default_filename)
    {
        mkdir(logdir.c_str(), 0775);
    }
    void Enable(int sty) //
    {
        style = sty;
    }
    std::string TimeStampExLocalTime()
    {
        time_t currtime = time(nullptr);
        struct tm* curr = localtime(&currtime);
        char time_buffer[128];
        snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",
            curr->tm_year + 1900, curr->tm_mon + 1, curr->tm_mday,
            curr->tm_hour, curr->tm_min, curr->tm_sec);
        return time_buffer;
    }
    void WriteLogToOneFile(const std::string& logname, const std::string& message)
    {
        umask(0);
        int fd = open(logname.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
        if (fd < 0) return;
        write(fd, message.c_str(), message.size());
        close(fd);
        // std::ofstream out(logname);
        // if (!out.is_open())
        //     return;
        // out.write(message.c_str(), message.size());
        // out.close();
    }
    void WriteLogToClassFile(const std::string& levelstr, const std::string& message)//将内容写入某个文件中
    {
        std::string logname = logdir;
        logname += "/";
        logname += filename;
        logname += levelstr;
        WriteLogToOneFile(logname, message);
    }

    void WriteLog(const std::string& levelstr, const std::string& message)//决定日志写入的位置:屏幕/单个文件/多个文件
    {
        switch (style)
        {
        case Screen:
            std::cout << message;
            break;
        case OneFile:
            WriteLogToClassFile("all", message);//单个文件
            break;
        case ClassFile:
            WriteLogToClassFile(levelstr, message);//多个文件
            break;
        default:
            break;
        }
    }
    void LogMessage(int level, const char* format, ...) // 类C的一个日志接口
    {
        char leftbuffer[1024];
        std::string levelstr = LevelToString(level);
        std::string currtime = TimeStampExLocalTime();
        std::string idstr = std::to_string(getpid());

        char rightbuffer[1024];
        va_list args; // char *, void *
        va_start(args, format);
        // args 指向了可变参数部分
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, args);
        va_end(args); // args = nullptr;
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%s][%s] ",
            levelstr.c_str(), currtime.c_str(), idstr.c_str());

        std::string loginfo = leftbuffer;
        loginfo += rightbuffer;
        WriteLog(levelstr, loginfo);
    }
    // void operator()(int level, const char *format, ...)
    // {
    //     LogMessage(int level, const char *format, ...)
    // }
    ~Log() {}

private:
    int style;
    std::string filename;
};

Log lg;

class Conf
{
public:
    Conf()
    {
        lg.Enable(Screen);
    }
    ~Conf()
    {}
};

Conf conf;

ComErr.hpp(放置错误码)

#pragma once

enum
{
    Usage_Err = 1,
    Socket_Err,
    Bind_Err
};

Main.cc

#include "Udpserver.hpp"
#include "ComErr.hpp"
#include <memory>

void Usage(std::string proc)
{
    std::cout << "Usage : \n\t" << proc << " local_ip local_port\n" << std::endl;
}
int main(int argc,char* argv[3])
{
    if(argc!=3)//如果命令行指令数没有三个
    {
        Usage(argv[0]);
        return Usage_Err;
    }
    string ip = argv[1];//获取ip
    uint16_t port = stoi(argv[2]);//获取端口号

    //接下来创建Udpserver的指针

    unique_ptr<Udpsever> usvr = std::make_unique<Udpsever>(ip,port);//unique_ptr不支持拷贝构造和赋值操作,因此确保了资源的独占性
    //使用make_unique记得添加memory头文件
    usvr->Init();
    usvr->Start();

    return 0;
}

UdpServer.hpp(服务端代码)

#include<iostream>
#include "ComErr.hpp"
#include"Log.hpp"
#include <cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include<cerrno>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>

using namespace std;

const string defaultip = "127.0.0.0";
const uint16_t defaultport = 8888;
class Udpsever
{
public:
    Udpsever(Udpsever& udp)=delete;//防止拷贝
    Udpsever(const Udpsever& udp)=delete;
    const Udpsever& operator=(Udpsever& udp)=delete;

    Udpsever(const string& ip = defaultip,const uint16_t& port = defaultport)
    :_ip(ip),_port(port)
    {}

    void Init()
    {
        //1.创建套接字对象
        _sockfd = socket(AF_INET,SOCK_DGRAM,0);//创建一个套接字对象,协议族为IPv4,套接字类型是数据报套接字
        if(_sockfd<0)
        {
            //创建失败则打印错误日志
            lg.LogMessage(Fatal,"socket err,%d : %s\n",errno,strerror(errno));//strerror需要包含头文件cstring
            exit(Socket_Err);
        }
        //成功则打印返回的套接字描述符
        lg.LogMessage(Info,"sockfd : %d\n",_sockfd);

        //上述我们只是创建了一个套接字对象,但是我们还必须将套接字对象与特定的网络信息(ip地址+端口号)绑定,这样以后其它计算机才能根据这个网络信息找到我们然后进行通信
        //所以接下来我们要与网络信息绑定,才能实现在网络中进行通信
        //我们要用到socket编程中的bind函数:用于将套接字与特定的地址(IP地址和端口号)绑定在一起,以便其他计算机可以找到并与之通信
        //2.bind并指定网络信息
        //2.1指定网络地址信息
        
        struct sockaddr_in local;//创建一个网络地址信息结构体,接下里初始化地址信息
        bzero(&local,sizeof(local));//相当于memset
        local.sin_family = AF_INET;//1.初始化协议族
        local.sin_port = htons(_port);//2.将16位无符号短整数从主机字节序转换为网络字节序(大端序)
        local.sin_addr.s_addr = inet_addr(_ip.c_str());//3.初始化ip地址,这里我们想让_ip变为4字节且是网络字节序,所以使用转换函数inet_addr

        //2.2将网络地址信息与socket文件对象进行绑定
        //bind函数原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
        int bd = bind(_sockfd,(const struct sockaddr *)&local,sizeof(local));
        if(bd!=0)
        {
            lg.LogMessage(Fatal,"bind err :%d %s\n",errno,strerror(errno));
            exit(Bind_Err);
        }
        //至此socket初始化完成

        

    }
    void Start()
    {
        int defaultbuffer = 1024;
        char buffer[defaultbuffer];
        while(true)//服务器永远不退出
        {
            //接受信息
            struct sockaddr_in peer;//用来存储发送方的地址信息
            socklen_t len = sizeof(peer);
            ssize_t n = recvfrom(_sockfd,buffer,sizeof(buffer)-1/*再减1是给\0留位置*/,0,(struct sockaddr*)&peer,&len);
            if(n!=-1)
            {
                //说明接受信息成功
                //接下来我们将打印发送方的信息
                buffer[n] = '\0';
                cout<<"Client say : "<<buffer<<endl;
                //接下来也发送一些信息回应客户端
                char retstr[] = "Got it,processing...";
                sendto(_sockfd,retstr,sizeof(retstr),0,(struct sockaddr*)&peer,len);
            }
        }
    }
private:
    string _ip;//ip地址
    uint16_t _port;//端口号
    int _sockfd;//套接字描述符
};

代码写完后,我们运行完成后。
再用netstat命令查看服务器状态,看看是否生成了我们的服务端服务器
在这里插入图片描述
可以看到确实出现了。

netstat

netstat 命令用于显示网络状态信息,包括网络连接、路由表、接口统计信息等。在 Linux 系统中,netstat 命令通常用于查看网络连接的状态、路由表、接口信息等,以便网络故障排除、性能监控等任务。

netstat 命令的基本语法如下:

netstat [options]

常用的选项包括:

  • -a:显示所有连接和监听端口。
  • -t:仅显示 TCP 协议的连接。
  • -u:仅显示 UDP 协议的连接。
  • -n:以数字形式显示地址和端口号,而不是以主机名和服务名显示。
  • -p:显示建立连接的进程标识符 PID 和程序名称。
  • -r:显示路由表信息。
  • -i:显示网络接口信息。

以下是一些常用的示例用法:

  1. 显示所有连接和监听端口:
netstat -a
  1. 仅显示 TCP 连接:
netstat -t
  1. 仅显示 UDP 连接:
netstat -u
  1. 以数字形式显示所有连接和监听端口:
netstat -an
  1. 显示建立连接的进程标识符 PID 和程序名称:
netstat -p
  1. 显示路由表信息:
netstat -r
  1. 显示网络接口信息:
netstat -i

除了上述选项之外,还有其他一些选项和参数可供选择,可以通过 man netstat 命令查看 netstat 命令的详细使用手册。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值