Linux网络编程 -- 网络套接字预备与udp

本文主要介绍网络编程的相关知识,在正式介绍网络编程之前,我们得先了解一些前置的知识。

1、端口号

我们上网其实就是两种动作,一个是将远处的数据拉取到本地,另一个是把我们的数据发送给远端。其实大部分的网络通信行为都是用户触发的,而在计算机中,谁表示用户呢?答案是进程。当用户接受到数据后,OS要将对应的数据给特定的服务进程。一台机器上有许多的服务进程,而我们用**特定的端口号(port)**来标识这个服务进程。

所以当我们将IP与端口号进行绑定,就可以得出互联网中的唯一一个进程了。这种方式也叫做套接字通信,服务器端和客户端的通信一般都采用这种方式。

为什么我们要用端口号来标识一个进程呢?不是有进程的pid吗?首先是因为进程的pid是不断变化的,其次如果使用pid的话,网络服务就和操作系统进行强关联了。此时如果我们修改一下操作系统对进程的标识方式,那么对上层的网络服务也需要进行大幅修改。另外OS中并不是每个进程都有端口号的,一般只有网络进程才有端口号。端口号和进程pid在OS会通过特定数据结构关联起来,例如哈希表。

2、TCP和UDP协议

在正式开始介绍网络编程之前,我们先简单了解一下UDP和TCP协议(后面具体介绍)

TCP(传输控制协议)和UDP(用户数据报协议)是互联网上用于数据传输的两种重要协议,它们的主要区别在于以下几个方面:

  1. 连接性

    • TCP 是面向连接的协议,意味着在数据传输之前,需要建立一个连接,在传输完成后需要断开连接。
    • UDP 是无连接的,它发送数据之前不需要建立连接,数据可以直接发送给接收方。
  2. 可靠性

    • TCP 提供了可靠的服务。它确保数据包的顺序传输,并且通过确认和重传机制保证数据的完整性。
    • UDP 不保证数据的可靠传输,它只负责发送数据,不保证数据包的顺序或是否到达。
  3. 速度与效率

    • TCP 由于其可靠性机制,速度相对较慢,因为它需要时间来建立连接、确认数据包和进行重传。
    • UDP 由于没有这些机制,通常更快,适用于对实时性要求较高的应用,如视频会议和在线游戏。
  4. 数据流控制

    • TCP 有流量控制和拥塞控制机制,可以根据网络状况调整数据传输的速度。
    • UDP 没有这样的控制机制,发送速率不会因网络状况而改变。
  5. 用途

    • TCP 常用于要求高可靠性的应用,如网页浏览、电子邮件和文件传输。
    • UDP 常用于实时应用,如流媒体、VoIP(网络电话)和在线游戏,这些应用对速度的要求高于数据完整性。
  6. 头部开销

    • TCP 头部较大,因为它需要包含更多的信息来保证数据的可靠传输。
    • UDP 头部较小,处理起来更快,开销更小

这里只是简单介绍,看不懂没有关系,有个基础概念即可。

3、网络字节序

在我们机器中,分为大端机和小端机,当我们在网络进行传输时,可能会面临大端机向小端机传输数据的情况。此时接受方读取数据时就可能会出现异常,所以这里统一将网络的数据定为大端。但是每次都要我们手动对数据进行大小端的转换未免太过麻烦,所以系统为我们提供特定的接口统一转换,后续会对其进行介绍。

4、接口预备知识

socket编程,是有很多的种类,有的是专门用来本地通信的(Unix socket),有的是用来跨网络进行通信的(inet socket),有的是用来进行网络管理的(raw socket )。这些套接字类型非常多,为了减少学习的成本,linux编写者就决定让这些套接字使用统一的接口。而OS是由C语言进行编写的,涉及到统一类型的问题就必须和结构体相关联。其中有关套接字的结构体常见有三种。

我们常用的类型是struct sockaddr,但实际上我们用于存储数据的结构体是struct sockadd_in和struct sockaddr_un。与网络编程相关的接口都使用的是sockaddr结构体,所以我们需要先用sockaddr_in结构体存储数据,然后强转成sockaddr结构。这种特性和C++多态非常类似。这里我们着重关注sockaddr结构即可,当我们将sockaddr_in 结构强转成sockaddr后,相关的接口依然能够识别原来sockaddr_in结构体内的数据。(sockaddr_un同理)

5、相关接口的介绍与认识

1、socket

该函数用于创建套接字,第一个参数用于指定套接字的域,第二个参数是套接字的类型,第三个参数在前两个参数确定的情况下填零即可。该接口成功调用的返回值是一个文件描述符,失败就返回-1,网络套接字创建以后相当于绑定了该文件描述符。这里我们一般就使用AF_INET。

第一个参数列表

第二个参数列表(使用udp协议时,我们就需要将该参数设置成SOCK_DGRAM)

2、bind

在创建完套接字后,我们必须要将创建的套接字与端口号和ip地址进行绑定,也就是将网络服务与本地的文件描述符绑定(先这样理解)。所以这里我们需要引入一个接口bind

第一个参数表示创建套接字的文件描述符,第二个是网络套接字的相关结构体(就是我们上文所提到的sockaddr,不过实际上我们使用的是sockaddr_in),第三个参数表示第二个参数代表的结构体大小。

在使用struct sockaddr_in之前,我们首先要将其定义出来,初始化后再对各个成员进行初始化。

sin_family 我们一般初始化成AF_INET即可,而sin_port和sin_addr的初始化需要特别注意的是,这两个成员再初始化之前,我们都需要对其作主机序列转成网络序列的操作,也就是转成大端,除此之外,由于IP地址是点分十进制风格的字符串来表示的,所以在网络传输前,还要将其变成4字节的IP。在初始化IP地址时,我们发现sockaddr_in中,表示IP地址的成员是一个结构体,而这个结构体内只有一个成员,在初始化的时候需要注意以下。上述这些操作都不需要我们手动的实现,OS已经提供了相关的接口。

端口的主机序列转网络序列的相关接口(这里我们使用第二个接口,因为我们一般将端口设成16位)

当前IP的主机序列转网络序列并将其转成4字节IP的相关接口(这里我们一般使用第二个接口)

3、recvfrom

该接口用于接收网络中数据,第一个参数是服务端或客户端绑定的文件描述符,第二个参数是一个缓冲区的指针,用于存放接收的数据,第三个参数用于表示接收数据的长度,第四个参数是位掩码,用于控制该函数,我们通常填零即可(因情况而定),第五个参数也是一个输出型的参数,用于接收发送方的信息,第六个参数用于表示第五个参数的大小。返回值表示实际读取到的数据长度。

4、sendto

该接口用于向特定主机发送数据,第一个参数服务端或客户端绑定的文件描述符,第二个参数是一个缓冲区的指针,表示要发送的数据。第三个参数表示缓冲区的大小,第四个参数表示位掩码,表示对该函数的控制,一般设置为零即可,第五个参数表示目标主机的相关信息,第六个参数表示第五个参数的大小。返回值表示发送出去的数据长度。

示例代码(udp)

Main.cc(服务端)

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

void Usage()
{
    std::cout << "./Main.cc  server_port\n"<< std::endl;
}
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage();
        return 0;
    } 
    EnableScreen();
    //std::string server_ip = argv[1];
    int server_port = std::stoi(argv[1]);
    std::unique_ptr<Udpserver> ptr = std::make_unique<Udpserver>(server_port);
    ptr->InitServer();
    ptr->Start();
    return 0;
}

Udpserver.hpp

#pragma once
#include <iostream>
#include "Log.hpp"
#include <strings.h>
#include "Inetaddr.hpp"
#include <cstring>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
enum
{
    SOCKET = 1,
    BIND
};

class Udpserver
{
public:
    Udpserver(uint16_t port) : _port(port), _isrunning(false)
    {
    }
    void InitServer()
    {
        _fd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_fd < 0)
        {
            LOG(INFO, "socket fail")
            exit(SOCKET);
        }

        struct sockaddr_in infor;
        bzero(&infor, sizeof(infor));//清空数据
        infor.sin_family = AF_INET;
        // 主机序列转网络序列
        infor.sin_port = htons(_port);
        infor.sin_addr.s_addr = htonl(INADDR_ANY);
        // 绑定
        socklen_t len = (socklen_t)sizeof(infor);
        int count = bind(_fd, (struct sockaddr *)&infor, len);
        if (count < 0)
        {
            LOG(ERROR, "bind fail ...")
            exit(BIND);
        }
        LOG(INFO, "bind success")
    }
    void Start()
    {
        _isrunning = true;
        LOG(INFO,"begin server...")

        while (1)
        {
            char buffer[1024];
            memset(buffer, 0, sizeof(buffer));
            struct sockaddr_in src;
            socklen_t len = (socklen_t)sizeof(src);
            ssize_t rnum = recvfrom(_fd, buffer, sizeof(buffer), 0, (struct sockaddr *)&src, &len);
            if (rnum > 0)
            {
                buffer[1023] = 0;
                Inetaddr addr(&src);
                LOG(INFO, "receive informaiton success")
                printf("#[%s:%d]: %s\n",addr.IP().c_str(),addr.Port(),buffer);
                ssize_t snum = sendto(_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&src, len);
            }
        }
        _isrunning = false;
    }

    ~Udpserver()
    {

    }
private:
    int _fd;
    bool _isrunning;
    uint16_t _port;
    //std::string _IP;
};

client.cc(客户端)

#include <iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include "Log.hpp"
#include <strings.h>
#include <arpa/inet.h>
#include <cstring>
#include <netinet/in.h>
using namespace std;
void Usage()
{
    std::cout << "./Main.cc server_ip  server_port\n"
              << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage();
        return 0;
    }
    // 创建套接字
    int fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0)
    {
        LOG(FATAL, "socket fail...")
        exit(-1);
    }
    struct sockaddr_in client;
    bzero(&client, sizeof(client));
    client.sin_family = AF_INET;
    client.sin_port = htons(stoi(argv[2]));
    client.sin_addr.s_addr = inet_addr(argv[1]);
    // client 不需要显示地绑定客户端。OS会在client发送数据时,随机绑定一个端口号
    // 通信
    std::string message;
    while (1)
    {
        std::cout << "Please Enter: ";
        std::getline(std::cin, message);
        sendto(fd, message.c_str(), message.size(), 0, (struct sockaddr *)&client, sizeof(client));

        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        char buffer[1024];
        memset(buffer, 0 , sizeof(buffer));
        ssize_t n = recvfrom(fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
        if (n > 0)
        {
            std::cout << buffer << std::endl;
        }
    }
    return 0;
}

Ineraddr.hpp

#include<iostream>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>


class Inetaddr
{
private:
    void Init()
    {
        port = ntohs(_src->sin_port);
        ip = inet_ntoa(_src->sin_addr);
    }
public:
    Inetaddr(struct sockaddr_in* src):_src(src)
    {
        Init();
    }
    std::string IP()
    {
        return ip;
    }
    uint16_t Port()
    {
        return port;
    }
    ~Inetaddr()
    {
    }
private:
    struct sockaddr_in* _src;
    std::string ip;
    uint16_t port;
};

Log.hpp

#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <stdarg.h>
#include <time.h>
#include <pthread.h>
#include <fstream>
enum Level
{
    INFO = 0,
    DEBUG,
    WARNING,
    ERROR,
    FATAL

};
std::string Level_tostring(int level)
{
    switch (level)
    {
    case INFO:
        return "INFO";
    case DEBUG:
        return "DEBUG";
    case WARNING:
        return "ERROR";
    case ERROR:
        return "ERROR";
    case FATAL:
        return "FATAL";
    default:
        return "Unkown";
    }
}

pthread_mutex_t _glock = PTHREAD_MUTEX_INITIALIZER;
bool _is_save = false;
const std::string filename = "log.txt";

void SaveLog(const std::string context)
{
    std::ofstream infile;
    infile.open(filename,std::ios::app);
    if(!infile.is_open())
    {
        std::cout << "open file failed" << std::endl;
    }
    else
    {
        infile << context;
    }
    infile.close();
}
std::string Gettime()
{
    time_t cur_time = time(NULL);
    struct tm *time_data = localtime(&cur_time);
    if (time_data == nullptr)
    {
        return "None";
    }
    char buffer[1024];
    snprintf(buffer, sizeof(buffer), "%d-%d-%d %d:%d:%d",
             time_data->tm_year + 1900,
             time_data->tm_mon + 1,
             time_data->tm_mday,
             time_data->tm_hour,
             time_data->tm_min,
             time_data->tm_sec);
    return buffer;
}
void LogMessage(std::string filename, int line, bool issave, int level, const char *format, ...)
{
    std::string levelstr = Level_tostring(level);
    std::string time = Gettime();
    // 可变参数
    char buffer[1024];
    va_list args;
    va_start(args, format);
    vsnprintf(buffer, sizeof(buffer), format, args);
    va_end(args);

    std::string context = "[" + levelstr + "]" + "[" + time + "]" + "[" + "line : " + std::to_string(line) + "]" + "[" + filename + "]" + ": " + buffer;
    pthread_mutex_lock(&_glock);
    if(!issave)
    {
        std::cout << context << std::endl;
    }
    else{
        SaveLog(context);
    }
    pthread_mutex_unlock(&_glock);
}

#define LOG(level, format, ...)                                          \
    do                                                                   \
    {                                                                    \
        LogMessage(__FILE__, __LINE__, _is_save, level, format, ##__VA_ARGS__); \
    } while (0);
#define EnableFile()    \
    do                  \
    {                   \
        _is_save = true; \
    } while (0);
#define EnableScreen()   \
    do                   \
    {                    \
        _is_save = false;\
    } while (0);

代码编写中及测试过程中的注意事项

<1>在客户端中,我们是不需要显示地绑定一个端口号的,操作系统会第一次连接时自动帮我们绑定一个随机端口号。如果帮显示绑定一个端口号,那么可能就会造成以下情况,当你的主机上需要同时启动两款App,但是这两款App绑定了同一个端口号,此时就会造成一个App启动后,另一个个App启动失败。

<2>当我们接收到sockadd_in结构体后,如果需要打印结构体内的信息,需要对对其进行从网络序列转成主机序列的操作,这也就是Inetaddr文件存在的原因(这里我封装了)。

<3>服务端不推荐绑定固定的ip(我们一般就绑定为0,表示能够处理任何ip发送的服务)。在云服务器上也不允许绑定公网的ip,如果需要在云服务器上绑定ip,则需要在对应的云服务的安全组上添加对应的端口。

以上就是所有内容

相关推荐
全能全知者27 分钟前
CentOS 快速部署 MinIO 教程
linux·运维·centos
戏神44 分钟前
基于H3C环境的实验——OSPF
运维·网络·网络协议·ospf
投机风清扬1 小时前
Linux内核参数管理
linux·运维
hgdlip1 小时前
手机一键换IP地址软件:功能、应用与选择指南‌
网络·网络协议·tcp/ip·手机
三掌柜6661 小时前
2024三掌柜赠书活动第三十二期:渗透测试理论与实践
网络·安全·web安全
x18783375841 小时前
k8s的安装和部署
linux·服务器·kubernetes
南希夜酒1 小时前
在Linux系统安装Nginx
java·linux·运维·服务器·centos·云计算
微刻时光1 小时前
Docker镜像命令汇总笔记
linux·运维·笔记·ubuntu·docker·容器·centos
大吱佬1 小时前
CentOS快速配置网络&&Docker快速部署
linux·服务器·tcp/ip
ZachOn1y1 小时前
计算机网络:数据链路层 —— 数据链路层概述
网络·计算机网络·数据链路层·知识点汇总·考研必备