TCP/UDP 通用通信代码库(C语言实现)

一、C 语言网络编程基础

(一)套接字概念与特点

介绍套接字是网络通信中的端点,具有跨进程、跨网络、双向通信、协议无关性和地址绑定等特点。

套接字作为网络通信的端点,实现了不同进程之间的数据交换,无论这些进程是在同一台机器上还是分布在不同的网络位置。跨进程通信使得不同的程序能够协同工作,共同完成复杂的任务。跨网络通信则打破了物理位置的限制,只要网络可达,不同地点的进程就可以进行通信。双向通信意味着套接字既可以发送数据,也可以接收数据,为交互性的应用提供了基础。协议无关性使得套接字可以支持多种网络协议,不仅仅局限于特定的一种,增加了其灵活性和适用性。而地址绑定则为每个套接字分配了唯一的地址,包括 IP 地址和端口号,确保数据能够准确地发送到目标套接字。

(二)头文件 <sys/socket.h> 概述

1.数据类型:socklen_t、sockaddr 等结构体的作用和意义。

  • socklen_t 是一种无符号整型,通常用于表示与套接字相关的参数大小,如地址长度等,确保了跨平台的兼容性。在不同的操作系统中,对于地址长度等参数的表示可能会有所不同,socklen_t 的使用可以避免这种差异带来的问题。
  • sockaddr 是一个通用的套接字地址结构,它被设计为一个抽象层,允许应用程序通过同一接口处理不同类型的网络协议和地址族。sockaddr 通常与更具体的结构体如 sockaddr_in(用于 IPv4)和 sockaddr_in6(用于 IPv6)结合使用。这些具体的结构体包含了特定协议所需的地址信息,如 IP 地址和端口号等。

2.常量:AF_INET、SOCK_STREAM 等常量的含义及用途。

  • AF_INET 是地址族常量,标识 IPv4 协议。在网络编程中,当我们使用 AF_INET 时,意味着我们将使用 IPv4 地址进行通信。IPv4 地址由四个字节组成,通常以点分十进制形式表示,如 192.168.1.1
  • SOCK_STREAM 是套接字类型常量,对应 TCP(面向连接的流套接字)。使用 SOCK_STREAM 类型的套接字可以实现可靠的数据传输,数据按序到达,不会出现丢失、重复或乱序的情况。这种类型的套接字适用于对数据传输可靠性要求较高的应用,如文件传输、电子邮件等。

3.函数:socket、bind、listen 等函数的功能和参数解析。

  • socket 函数用于创建一个新的套接字,它的原型为 int socket(int domain, int type, int protocol)。其中,domain 参数指定通信域(地址族),如 AF_INET(IPv4)或 AF_INET6(IPv6);type 参数指定套接字类型,如 SOCK_STREAM(TCP)或 SOCK_DGRAM(UDP);protocol 参数指定使用的协议,一般设置为 0,让系统自动选择与 domain 和 type 匹配的默认协议。如果成功创建套接字,返回一个非负整数,即套接字描述符,用于后续的套接字操作;如果失败,返回 -1,并设置全局变量 errno 为相应的错误代码。
  • bind 函数将一个套接字与本地地址(IP 地址和端口号)绑定,使得该套接字可以接收发往该地址的连接请求或数据报。它的原型为 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)。sockfd 是由 socket 函数返回的套接字描述符,addr 是一个指向包含本地地址信息的 sockaddr 结构体的指针,addrlen 是 addr 所指向的结构体的长度。
  • listen 函数使一个 TCP 套接字进入被动监听状态,等待客户端的连接请求。它的原型为 int listen(int sockfd, int backlog)。sockfd 是要监听的套接字描述符,backlog 参数指定同时可接纳的最大连接请求队列长度。

二、TCP 通信实现

(一)服务器端流程

1.创建套接字:socket 函数参数及返回值解析。

  • socket函数用于创建一个新的套接字,其原型为int socket(int domain, int type, int protocol)。其中,domain参数指定通信域,通常使用AF_INET表示 IPv4 地址族。type参数指定套接字类型,对于 TCP 通信,使用SOCK_STREAM表示面向连接的流套接字。protocol参数指定使用的协议,一般设置为 0,让系统自动选择与domain和type匹配的默认协议,通常为IPPROTO_TCP。如果成功创建套接字,返回一个非负整数,即套接字描述符,用于后续的套接字操作;如果失败,返回 -1,并设置全局变量errno为相应的错误代码。

2.绑定地址:sockaddr_in 结构体与 bind 函数的使用。

  • sockaddr_in结构体用于存储 IPv4 地址和端口号等信息。其成员包括sin_family(通常设置为AF_INET)、sin_port(存储端口号,需使用htons函数将主机字节序转换为网络字节序)、sin_addr(存储 IP 地址,可使用inet_addr函数将点分十进制形式的 IP 地址转换为整数形式)和sin_zero(一般用 0 填充,确保结构体大小与sockaddr结构体一致)。bind函数将一个套接字与本地地址(IP 地址和端口号)绑定,其原型为int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)。sockfd是由socket函数返回的套接字描述符,addr是一个指向sockaddr_in结构体的指针,需强制转换为const struct sockaddr *类型,addrlen是addr所指向的结构体的长度,可由sizeof计算得出。

3.监听连接:listen 函数设置被动监听状态。

  • listen函数使一个 TCP 套接字进入被动监听状态,等待客户端的连接请求。其原型为int listen(int sockfd, int backlog)。sockfd是要监听的套接字描述符,backlog参数指定同时可接纳的最大连接请求队列长度。例如,设置backlog为 10,表示服务器可以同时处理 10 个未完成的连接请求。

3.接受连接:accept 函数阻塞等待客户端连接。

  • accept函数从已连接的队列中取出一个已经建立的连接,如果没有任何连接,则进入睡眠等待(阻塞)状态。其原型为int accept(int sockfd, struct sockaddr* cliaddr, socklen_t *addrlen)。sockfd是socket监听套接字,cliaddr用于存放客户端套接字地址结构,addrlen是套接字地址结构体长度的地址。如果成功返回已连接的套接字描述符,这个新的描述符专门与指定的客户端通信;如果失败,返回 -1。

4.收发数据:read/write 函数实现数据交互。

  • 使用read函数从已连接的套接字中读取数据,其原型为ssize_t read(int fd, void *buf, size_t count)。fd是已连接的套接字描述符,buf是存储读取数据的缓冲区地址,count是要读取的字节数。成功返回读取的字节数,失败返回 -1。类似地,使用write函数向已连接的套接字写入数据,其原型为ssize_t write(int fd, const void *buf, size_t count)。fd是已连接的套接字描述符,buf是要写入数据的缓冲区地址,count是要写入的字节数。成功返回写入的字节数,失败返回 -1。

5.关闭套接字:释放资源。

  • 在服务器端完成通信后,需要关闭套接字以释放资源。可以使用close函数关闭套接字描述符,其原型为int close(int fd)。fd是要关闭的套接字描述符。关闭套接字后,操作系统将回收相关资源,避免资源泄漏。

(二)客户端流程

1.创建套接字。

  • 客户端同样使用socket函数创建一个套接字,参数与服务器端创建套接字时相同。成功创建后得到一个套接字描述符,用于后续的连接和通信操作。

2.填充服务器网络信息结构体。

  • 创建一个sockaddr_in结构体,设置其成员变量。将sin_family设置为AF_INET,表示 IPv4 地址族。使用inet_addr函数将服务器的 IP 地址转换为整数形式,并赋值给sin_addr成员。使用htons函数将服务器的端口号转换为网络字节序,并赋值给sin_port成员。

3.建立连接:connect 函数与服务器建立连接。

  • connect函数用于建立与服务器的连接,其原型为int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)。sockfd是客户端创建的套接字描述符,addr是指向服务器地址结构体的指针,addrlen是地址结构体的长度。如果连接成功,返回 0;如果失败,返回 -1,并设置相应的错误码。

4.收发数据。

  • 客户端可以使用send函数向服务器发送数据,其原型为ssize_t send(int sockfd, const void *buf, size_t len, int flags)。sockfd是已连接的套接字描述符,buf是要发送数据的缓冲区地址,len是要发送的字节数,flags通常设置为 0。成功返回发送的字节数,失败返回 -1。使用recv函数接收服务器发送的数据,其原型为ssize_t recv(int sockfd, void *buf, size_t len, int flags)。参数含义与send函数类似。成功返回接收到的字节数,失败返回 -1。如果发送端关闭文件描述符或者关闭进程,recv函数会返回 0。

5.关闭套接字。

  • 通信结束后,客户端使用close函数关闭套接字描述符,释放资源。与服务器端关闭套接字的方式相同。

三、UDP 通信实现

(一)服务器端流程

1.创建套接字。

  • 服务器端首先使用socket函数创建一个套接字,与 TCP 通信类似,其原型为int socket(int domain, int type, int protocol)。对于 UDP 通信,domain参数通常设置为AF_INET表示 IPv4 地址族,type参数设置为SOCK_DGRAM表示数据报套接字,protocol参数一般设置为 0,让系统自动选择与domain和type匹配的默认协议,通常为IPPROTO_UDP。如果成功创建套接字,返回一个非负整数,即套接字描述符,用于后续的操作;如果失败,返回 -1,并设置全局变量errno为相应的错误代码。

2.绑定地址。

  • 接着,服务器需要绑定地址,使用bind函数将套接字与本地地址(IP 地址和端口号)绑定,使得该套接字可以接收发往该地址的 UDP 数据报。其原型为int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)。sockfd是由socket函数返回的套接字描述符,addr是一个指向包含本地地址信息的sockaddr_in结构体的指针,需强制转换为const struct sockaddr *类型。sockaddr_in结构体用于存储 IPv4 地址和端口号等信息,成员sin_family设置为AF_INET,sin_port存储端口号,需使用htons函数将主机字节序转换为网络字节序,sin_addr存储 IP 地址,可使用inet_addr函数将点分十进制形式的 IP 地址转换为整数形式,sin_zero一般用 0 填充,确保结构体大小与sockaddr结构体一致。addrlen是addr所指向的结构体的长度,可由sizeof计算得出。

3.接收数据:recvfrom 函数获取数据及来源地址。

  • 使用recvfrom函数接收 UDP 数据报,并获取数据的来源地址信息。其原型为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类型变量的指针,用于存储src_addr所指向的结构体的长度。成功时,recvfrom函数返回实际接收的字节数;失败时,返回 -1,并设置errno为相应的错误代码。

4.发送数据:sendto 函数发送数据至指定地址。

  • 当服务器需要回复客户端时,可以使用sendto函数发送数据至指定地址。其原型为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是dest_addr所指向的结构体的长度。成功时,sendto函数返回实际发送的字节数;失败时,返回 -1,并设置errno为相应的错误代码。

5.关闭套接字。

  • 通信结束后,服务器使用close函数关闭套接字描述符,释放资源。其原型为int close(int fd),fd是要关闭的套接字描述符。关闭套接字后,操作系统将回收相关资源,避免资源泄漏。

(二)客户端流程

1.创建套接字。

  • 客户端同样使用socket函数创建一个套接字,参数与服务器端创建套接字时相同,用于 UDP 通信。成功创建后得到一个套接字描述符,用于后续的发送和接收操作。

2.发送数据:sendto 函数向服务器发送数据。

  • 客户端使用sendto函数向服务器发送数据。其参数与服务器端发送数据时的参数类似,sockfd是客户端创建的套接字描述符,buf是要发送数据的缓冲区地址,len是要发送数据的长度,flags一般设置为 0,dest_addr是一个指向sockaddr结构体的指针,包含服务器的地址信息,addrlen是dest_addr所指向的结构体的长度。成功时,sendto函数返回实际发送的字节数;失败时,返回 -1,并设置errno为相应的错误代码。

3.接收数据:recvfrom 函数接收服务器返回的数据。

  • 客户端使用recvfrom函数接收服务器返回的数据。其参数与服务器端接收数据时的参数类似,sockfd是套接字描述符,buf是存储接收数据的缓冲区地址,len是缓冲区的长度,flags一般设置为 0,src_addr是一个指向sockaddr结构体的指针,用于存储数据来源的地址信息(在客户端通常不需要关注这个地址,因为数据来源通常只有服务器),addrlen是一个指向socklen_t类型变量的指针,用于存储src_addr所指向的结构体的长度。成功时,recvfrom函数返回实际接收的字节数;失败时,返回 -1,并设置errno为相应的错误代码。

4.关闭套接字。

  • 通信结束后,客户端使用close函数关闭套接字描述符,释放资源,与服务器端关闭套接字的方式相同。

四、通用通信代码库封装

(一)封装思路与结构

在 C 语言中,为了实现 TCP 和 UDP 的通用通信代码库封装,我们采用了结构体和一系列函数的方式。结构体 Network 定义了通信所需的关键元素,包括通信协议类型、套接字描述符、通讯地址、通讯地址字节数以及是否为服务端的标志。这个结构体的设计使得我们可以在不同的通信场景中,无论是 TCP 还是 UDP,都能够方便地管理和操作通信对象。

通过封装一系列功能函数,我们将复杂的网络通信操作进行了抽象和简化。每个函数都有明确的职责,分别负责创建和初始化网络对象、等待连接、发送和接收数据、关闭套接字以及获取 IP 地址等操作。这种封装思路使得代码更加模块化、易于维护和扩展,同时也提高了代码的可读性和可重用性。

(二)函数声明

cpp 复制代码
#ifndef NETWORK_H
#define NETWORK_H

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

typedef struct NetWork
{
	int type;		//通信协议类型
	int sock_fd;	//socket描述符
	struct sockaddr_in addr;//通信地址
	socklen_t addrlen;		//通信地址字节数
	bool issvr;		//是否是服务器
}NetWork;

typedef struct sockaddr* SP;

//分配内存,创建socket套接字,初始化地址,绑定,监听,连接
NetWork* init_nw(int type,short port,const char* ip,bool issvr);

//等待连接,只有tcp协议的服务端才能调用
NetWork* accept_nw(NetWork* svr_nw);

//具备send和sendto的功能
int send_nw(NetWork* nw,void* buf,size_t len);

//具备recv和recvfrom的功能
int recv_nw(NetWork* nw,void* buf,size_t len);

//关闭socket对象,并释放内存
void close_nw(NetWork* nw);

//获取ip地址
const char* getip_nw(NetWork* nw);


#endif//NETWORK_H

(三)函数声明详解

一、功能概述

这个头文件定义了一个用于网络通信的结构体NetWork和一系列与之相关的函数。它提供了创建网络连接、发送和接收数据、关闭连接以及获取 IP 地址等功能。

二、结构体定义

  1. NetWork结构体:
    • type:表示通信协议类型,可能是SOCK_STREAM(TCP)或SOCK_DGRAM(UDP)等。
    • sock_fd:socket 描述符,用于标识一个网络连接。
    • addrsockaddr_in结构体,包含了 IP 地址和端口号等通信地址信息。
    • addrlen:通信地址的字节数。
    • issvr:表示是否是服务器端。

三、函数声明

  1. init_nw

    • 功能:分配内存,创建 socket 套接字,初始化地址,根据传入的参数进行绑定(如果是服务器端)或连接(如果是客户端)操作。
    • 参数:
      • type:通信协议类型。
      • port:端口号。
      • ip:IP 地址字符串。
      • issvr:是否是服务器端的标志。
    • 返回值:指向NetWork结构体的指针,如果创建失败则返回NULL
  2. accept_nw

    • 功能:只有在 TCP 协议的服务器端才能调用,用于等待客户端的连接请求,并返回一个新的NetWork结构体指针表示与客户端的连接。
    • 参数:NetWork* svr_nw,指向服务器端的NetWork结构体指针。
    • 返回值:指向新的NetWork结构体的指针,如果接受连接失败则返回NULL
  3. send_nw

    • 功能:具备send(用于面向连接的通信)和sendto(用于无连接的通信)的功能,根据通信协议类型发送数据。
    • 参数:
      • NetWork* nw:指向要发送数据的连接的NetWork结构体指针。
      • buf:指向要发送数据的缓冲区的指针。
      • len:要发送数据的长度。
    • 返回值:实际发送的字节数,如果发送失败则返回 -1。
  4. recv_nw

    • 功能:具备recv(用于面向连接的通信)和recvfrom(用于无连接的通信)的功能,根据通信协议类型接收数据。
    • 参数:
      • NetWork* nw:指向要接收数据的连接的NetWork结构体指针。
      • buf:指向接收数据的缓冲区的指针。
      • len:接收缓冲区的长度。
    • 返回值:实际接收的字节数,如果接收失败则返回 -1。
  5. close_nw

    • 功能:关闭 socket 对象,并释放与之相关的内存。
    • 参数:NetWork* nw,指向要关闭的连接的NetWork结构体指针。
  6. getip_nw

    • 功能:获取指定连接的 IP 地址。
    • 参数:NetWork* nw,指向要获取 IP 地址的连接的NetWork结构体指针。
    • 返回值:指向 IP 地址字符串的常量指针。

四、注意事项

  1. 在使用这些函数时,需要确保正确包含了所需的头文件,并处理可能出现的错误情况。例如,在创建 socket 时可能会失败,发送和接收数据时可能会遇到网络问题等。
  2. 对于服务器端,需要先调用init_nw创建服务器连接,然后调用listen(如果是 TCP 协议)等待客户端连接,并使用accept_nw接受连接请求。对于客户端,只需要调用init_nw进行连接即可。
  3. 在使用完网络连接后,一定要调用close_nw关闭连接并释放资源,以避免资源泄漏。

(四)函数定义

cpp 复制代码
#include "network.h"
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>

//分配内存,创建socket套接字,初始化地址,绑定,监听,连接
NetWork* init_nw(int type,short port,const char* ip,bool issvr)
{
    // 分配内存创建一个 NetWork 结构体指针 nw
    NetWork* nw = malloc(sizeof(NetWork));
    // 设置 nw 的通信协议类型和是否为服务器端标志
    nw->type = type;
    nw->issvr = issvr;
    // 创建 socket 套接字
    nw->sock_fd = socket(AF_INET,type,0);
    if(nw->sock_fd<0)
    {
        // 如果创建失败,释放内存并返回 NULL
        free(nw);
        perror("socket");
        return NULL;
    }
    // 初始化地址结构体
    bzero(&nw->addr,nw->addrlen);
    // 准备通信地址
    nw->addr.sin_family = AF_INET;
    nw->addr.sin_port = htons(port);
    // 将 IP 地址字符串转换为网络地址格式
    nw->addr.sin_addr.s_addr = inet_addr(ip);
    nw->addrlen = sizeof(struct sockaddr_in);

    if(issvr)
    {
        // 如果是服务器端,进行绑定操作
        if(bind(nw->sock_fd,(SP)&nw->addr,nw->addrlen))
        {
            // 如果绑定失败,释放内存并返回 NULL
            free(nw);
            perror("bind");
            return NULL;
        }
        // 如果是 TCP 协议的服务器端,进行监听操作
        if(SOCK_STREAM == type && listen(nw->sock_fd,50))
        {
            free(nw);
            perror("listen");
            return NULL;
        }
    }
    else if(SOCK_STREAM == type)//TCP 客户端
    {
        // 如果是 TCP 客户端,进行连接操作
        if(connect(nw->sock_fd,(SP)&nw->addr,nw->addrlen))
        {
            free(nw);
            perror("connect");
            return NULL;
        }
    }

    return nw;
}

//等待连接,只有 tcp 协议的服务端才能调用
NetWork* accept_nw(NetWork* svr_nw)
{
    if(SOCK_STREAM!= svr_nw->type||!svr_nw->issvr)
    {
        // 如果输入的不是 TCP 协议的服务器端,打印错误信息并返回 NULL
        printf("只有 TCP 协议且为服务器端的 NetWork 对象才能调用\n");
        return NULL;
    }
    // 为新的 NetWork 分配内存并初始化
    NetWork* nw = malloc(sizeof(NetWork));
    nw->addrlen = svr_nw->addrlen;
    nw->type = SOCK_STREAM;
    nw->issvr = true;

    // 接受客户端连接
    nw->sock_fd = accept(svr_nw->sock_fd,(SP)&nw->addr,&nw->addrlen);
    if(nw->sock_fd < 0)
    {
        // 如果接受连接失败,释放内存并返回 NULL
        free(nw);
        perror("accept");
        return NULL;
    }
    return nw;
}

//具备 send 和 sendto 的功能
int send_nw(NetWork* nw,void* buf,size_t len)
{
    if(nw->type == SOCK_DGRAM)
    {
        // 如果是 UDP 协议,调用 sendto 函数发送数据
        return sendto(nw->sock_fd,buf,len,0,(SP)&nw->addr,nw->addrlen);
    }
    else
    {
        // 如果是 TCP 协议,调用 send 函数发送数据
        return send(nw->sock_fd,buf,len,0);
    }
}

//具备 recv 和 recvfrom 的功能
int recv_nw(NetWork* nw,void* buf,size_t len)
{
    if(nw->type == SOCK_DGRAM)
    {
        // 如果是 UDP 协议,调用 recvfrom 函数接收数据
        return recvfrom(nw->sock_fd,buf,len,0,(SP)&nw->addr,&nw->addrlen);
    }
    else
    {
        // 如果是 TCP 协议,调用 recv 函数接收数据
        return recv(nw->sock_fd,buf,len,0);
    }
}

//关闭 socket 对象,并释放内存
void close_nw(NetWork* nw)
{
    // 关闭 socket 描述符
    close(nw->sock_fd);
    // 释放内存
    free(nw);
}

//获取 ip 地址
const char* getip_nw(NetWork* nw)
{
    // 将网络地址转换为 IP 地址字符串并返回
    return inet_ntoa(nw->addr.sin_addr);
}
相关推荐
黄交大彭于晏24 分钟前
第五天学习总结:C语言学习笔记 - 数组篇
c语言·笔记·学习
何曾参静谧1 小时前
「C/C++」C++20 之 #include<ranges> 范围
c语言·c++·c++20
码出钞能力1 小时前
UDP组播测试
网络·网络协议·udp
不甘平凡的蜜蜂1 小时前
第三十三篇:TCP协议如何避免/减少网络拥塞,TCP系列八
运维·网络·网络协议·tcp/ip·计算机网络·智能路由器
bitenum3 小时前
qsort函数的学习与使用
c语言·开发语言·学习·算法·visualstudio·1024程序员节
wh233z3 小时前
Codeforces Round 981 (Div. 3) (A~F)
c语言·数据结构·c++·算法
白榆maple3 小时前
(蓝桥杯C/C++)——常用库函数
c语言·c++·蓝桥杯
梅见十柒4 小时前
数据结构与算法分析——你真的理解查找算法吗——基于散列的查找(代码详解+万字长文)
java·c语言·c++·笔记·算法·哈希算法·查找算法
知困勉行的Allen4 小时前
~C.库函数的介绍~
c语言·开发语言·数据结构·c++·学习方法
GGBondlctrl5 小时前
【JavaEE初阶】网络原理—关于TCP协议值滑动窗口与流量控制,进来看看吧!!!
网络·网络协议·tcp/ip·滑动窗口·流量控制·拥塞控制·tcp协议特性