计算机网络(二)——UDP网络编程套接字

本文介绍了网络编程的基础知识,包括端口号、TCP和UDP协议、网络字节序的概念,详细讲解了sockaddr结构体和socket编程接口。重点讨论了UDP服务端和客户端的实现,服务端通过popen函数执行客户端发送的命令并回应。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


一、预备知识

1.1 端口号

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址.
端口号(port)是传输层协议的内容.

  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用.

一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定;

传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要发给谁”;

1.2 TCP和UDP协议

两协议后面会细说
TCP(Transmission Control Protocol 传输控制协议)特点

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

UDP(User Datagram Protocol 用户数据报协议)

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

1.3 网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏
移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
  • 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端,就需要先将数据转成大端; 否则就忽略,直接发送即可;

为此系统也提供了四个转换函数
在这里插入图片描述

  • 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

1.4 sockaddr结构

Socket API ,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX DomainSocket。然而,各种网络协议的地址格式并不相同

在这里插入图片描述

  • 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结构体指针做为参数;
/* Structure describing a generic socket address.  */
struct sockaddr
  {
    __SOCKADDR_COMMON (sa_);	/* Common data: address family and length.  */
    char sa_data[14];		/* Address data.  */
  };
/* Structure describing an Internet socket address.  */
struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;			/* Port number.  */ // typedef uint16_t in_port_t
    struct in_addr sin_addr;		/* Internet address.  */
  };
/* Internet address.  */
typedef uint32_t in_addr_t;
struct in_addr
  {
    in_addr_t s_addr;
  };

1.5 socket常见编程接口

1.5.1 创建 socket 文件描述符

#include <sys/types.h> 
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

domain:地址域  该参数指定网络层使用什么协议
AF_INET:使用ipv4版本的ip协议
AF_INET6:使用ipv6版本的ip协议
AF_UNIX :本地域套接字(适用与一台机器两个进程,进行进程间通信)

type:套接字类型  指定传输层使用什么协议
SOCK_STREAM:流式套接字(TCP字节流)
SOCK_DGRAM:用户数据报套接字

protocol:使用的协议协议
0:采用套接字类型对应的默认协议
SOCK_DGRAM :默认的协议就是UDP
SOCK_STREAM:默认的协议就是TCP


返回值:
创建失败返回-1,成功返回值大于等于0(本质是一个文件描述符)

在Linux下一切皆文件,所以创建网络通信本质也是打开一个网络文件。

1.5.2 绑定端口号

需要将创建出来的socket文件和网络信息(sockaddr)进行绑定

#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address,
			socklen_t address_len);

sockfd:socket函数返回的套接字描述符;将创建出来的套接字和网卡,端口号进行绑定

address:给套接字绑定的sockaddr,里面包含ip地址和端口号

address_len:address的长度

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

1.5.3 接收网络信息

#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:socket返回的文件描述符
buf:读数据缓冲区
len:期望读取的数据长度
flags:读数据是IO、不一定有数据让你读,如果读取条件不成立,就挂起等待(默认为0,阻塞等待)
src_addr:用来获取发送方的sockaddr,也就是ip地址和端口号数据,如果不关心可以设置为空
addrlen:是一个整数,是实际读到结构体src_addr的大小,这个参数必须要进行初始化
src_addr和addrlen是输出型参数

返回值:
实际收到多少个字节的数据,如果为-1则接收错误
size_t无符号整形
ssize_t有符号整形

1.5.4 发送网络信息

#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:socket返回的文件描述符
buf:发送的数据
len:发送的长度
flags:如果发送条件不成立,就挂起等待(默认为0,阻塞等待)
src_addr:接收方的sockaddr,也就是ip地址和端口号数据
addrlen:dest_addr结构体的大小
返回值:
实际发送多少个字节的数据,如果为-1则发送错误

1.5.5 点分十进制字符串转二进制ip地址网络字节序

  1. 需要将人识别的点分十进制,字符串风格IP地址,转化成为4字节整数IP
  2. 也要考虑大小端

in_addr_t inet_addr(const char *cp); 能完成上面两个工作

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp); // typedef uint32_t in_addr_t;

二、UDP网络程序

UDP网络程序属于C/S 即客户端和服务器。

对于客户端而言

  • 不用显式绑定socket信息(ip和port)
    客户端也有自己的ip和port,一个客户端不是一直运行的,而且客户端一般来说是很多的,绑定一个固定的端口不一定会成功,在发送数据的时候,自动由OS绑定,采用的是随机端口,保证不会冲突

对于服务器而言

  • 一定要绑定ip和port,因为有这些客户端才能访问到服务器,ip和port是总所周知的,port一般不会轻易改变。

2.1 服务端

这里设置一个服务,读取到客户端发过来的命令并执行。这里使用到popen函数

#include <stdio.h>
FILE *popen(const char *command, const char *type);
command: 需要执行的命令
type: 读取返回文件的方式

返回值是一个文件指针,文件里面存放执行命令后得到的字符串

int pclose(FILE *stream); // 关闭文件
#include <iostream>
#include <cerrno>
#include <string>
#include <cstring>
#include <cstdio>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

//const uint16_t port = 9090;
// 由调用者设置端口,当然某些端口号不可用
std::string Usage(std::string proc)
{
    std::cout <<"Usage: "<< proc << " port" << std::endl;
}
int main(int argc, char* argv[])
{
    if(argc != 2){
        Usage(argv[0]);
        return -1;
    }
    
    uint16_t port = atoi(argv[1]);
    // 1. 创建套接字,打开网络文件
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket create error: " << errno << std::endl;
        return 1;
    }

    // 2. 给该网络文件(服务器)绑定端口和ip
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port); // 端口号属于计算机上的变量,是主机序列,要转成短整型的网络序列
    // 设置服务端的IP地址,一般不直接设主机的公网IP
    // 如果绑定了特定的IP地址,那么只能接受发送到该IP地址的信息
    // 但是如果这台主机有多张网卡多个IP,客户端的信息可能发给其他IP
    // 我们需要的数据是发送到该主机特定端口号的信息
    // INADDR_ANY的值为0,表示发送到该主机端口号的数据都接受
    local.sin_addr.s_addr = INADDR_ANY;

    if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
    {
        std::cerr << "bind error : " << errno << std::endl;
        return 2;
    }

    // 3. 提供服务
    bool quit = false; // 一直接收信息
    char buffer[1024];
    while (!quit)
    {
        sockaddr_in client;
        socklen_t len = sizeof(client);

        ssize_t s = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&client, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "client#" << buffer << std::endl;
            // 执行接受到的命令
            FILE* fp = popen(buffer, "r");
            char line[1024];
            std::string echo_com;
            while (fgets(line, sizeof(line), fp))
            {
                echo_com += line;
            }
            pclose(fp);
            sendto(sock, echo_com.c_str(), echo_com.size(), 0, (sockaddr *)&client, len);
        }
    }

    return 0;
}

2.2 客户端

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

void Usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << "server_ip server_port" << std::endl;
}

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

    // 1.创建套接字网络文件
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket error : " << errno << std::endl;
        return 1;
    }
    // 客户端不需要显式的绑定bind
    // a. 客户端也有自己的ip和port
    // b. 一个客户端不是一直运行的,而且客户端一般来说是很多的,绑定一个固定的端口不一定会成功
    // 不显式的绑定,在发送数据的时候,自动由OS绑定,采用的是随机端口,保证不会冲突
    //

    // 2.创建接受者的信息
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);

    // 3.使用服务
    while (true)
    {
        // 用户输入数据,并发送给服务器
        // std::string message;
        // std::cout << "输入信息#";
        // std::cin >> message;
        char line[1024];
        std::cout << "MyShell#";
        fgets(line, sizeof line, stdin);
        sendto(sock, line, sizeof(line) - 1, 0, (sockaddr *)&server, sizeof(server));

        // 接受服务器发送的信息
        char buffer[1024 * 4];
        int s = recvfrom(sock, buffer, sizeof(buffer), 0, nullptr, 0);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << buffer << std::endl;
        }
    }
    return 0;
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

s_persist

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值