Linux 网络编程(二)——套接字编程简介

文章目录

[2 Socket 套接字](#2 Socket 套接字)

[2.1 什么是 Socket](#2.1 什么是 Socket)

[2.2 Socket编程的基本操作](#2.2 Socket编程的基本操作)

[2.3 地址信息的表示](#2.3 地址信息的表示)

[2.4 网络字节序和主机字节序的转换](#2.4 网络字节序和主机字节序的转换)

[2.4.1 字节序转换](#2.4.1 字节序转换)

[2.4.2 网络地址初始化与分配](#2.4.2 网络地址初始化与分配)

[2.5 INADDR_ANY](#2.5 INADDR_ANY)

[2.6 Socket 编程相关函数](#2.6 Socket 编程相关函数)

[2.7 C标准中的 main 函数声明](#2.7 C标准中的 main 函数声明)

[2.8 套接字应用示例](#2.8 套接字应用示例)


2 Socket 套接字

2.1 什么是 Socket

套接字(Socket)是计算机网络数据通信的基本概念和编程接口,允许不同主机上的进程通过网络进行数据交换。一个套接字主要由以下三个部分组成

  • 网络地址:通常是 IP 地址,用于标识网络上的设备

  • 端口号:端口号在同一操作系统内区分不同套接字而设计的,用于标识设备上的特定应用或进程,端口号是一个16位数字,范围是0到65565

  • 协议:定义数据传输的规则和格式,如TCP和UDP

2.2 Socket编程的基本操作

  • 创建Socket:通过 socket() 系统调用创建一个 Socket,该调用返回 int 类型的文件描述符。
  • 绑定地址:对于服务器端,需要使用 bind() 系统调用将 Socket 绑定到一个特定的 IP 地址和端口号上,以便客户端能够连接。(客户端可绑可不绑)
  • 监听连接请求(仅适用于TCP):对于TCP服务器,需要使用 listen() 系统调用开始监听客户端的连接请求。
  • 接受连接(仅适用于TCP):使用 accept() 系统调用接受客户端的连接请求,返回一个新的 Socket 描述符,通过新的 Socket 描述符与客户端进行通信。
  • 发起连接(仅适用于TCP客户端):对于TCP客户端,使用 connect() 系统调用向服务器端发起连接请求。
  • 发送和接收数据:通过 send() 和 recv() 系统调用(对于UDP, 使用sendto() 和 recvfrom() 系统调用)发送和接收数据。
  • 关闭连接:通信结束后,使用 close() 系统调用关闭 Socket 连接。

2.3 地址信息的表示

在 Socket 编程中,应用程序使用的 IP 地址和端口号以结构体的形式进行了定义。Socket 编程的核心任务之一就是要正确填写主机地址和目标地址,因此,理解这些结构体的定义和使用方法至关重要。

cpp 复制代码
struct sockaddr {
    sa_family_t		sin_family;		//地址族,IPV4:AF_INET IPv6:AF_INET6
    char            sa_data[14];    //地址信息
}
-----------------------------------------------------------------------
struct sockaddr_in {
    sa_family_t		sin_family;		//地址族,IPV4:AF_INET IPv6:AF_INET6
    uint16_t		sin_port;		//16位TCP/UDP端口号,以网络字节序保存
    struct in_addr	sin_addr;		//32位IP地址,以网络字节序保存
    char			sin_zero[8];	//不使用
}

struct in_addr {
    In_addr_t	s_addr;		//32位IPv4地址
}

分割线上的 sockaddr 结构体是 Socket 官方文档中函数的参数,但它仅包含一个地址族字段和一个字符数组用于存储地址数据,无法直接表示 IP 地址和端口号。因此,在实际开发中,我们通常会使用 sockaddr_in 来表示地址字段,然后通过强制类型转换将其转换为 sockaddr 类型。此外,在 sockaddr_in 中,还 "套娃" 了一个 in_addr 结构体,用于存放 IP 地址。(代码块中的数据类型可以参考 POSIX 文档)

2.4 网络字节序和主机字节序的转换

CPU 向内存保存数据的方式有两种,不同的 CPU 保存数据的方式可能也会有所不同

  • 大端字节序(大端存储):高位字节存放低地址部分
  • 小端字节序(小端存储):高位字节存放高地址部分

网络字节序是一种标准的字节存储顺序,用于在网络上传输数据。网络字节序是大端字节序,也就是说,当数据在网络上传输时,无论发送方和接收方的主机字节序是什么保存格式,都必须将数据转换为大端字节序进行传输。

2.4.1 字节序转换

cpp 复制代码
// 将 32 位整数 从主机字节序转换为网络字节序        "Host to Network Long int"  
uint32_t htonl(uint32_t hostlong);

// 将 32 位整数 从网络字节序转换为主机字节序        "Network to Host Long int"  
uint32_t ntohl(uint32_t netlong);

// 将 16 位整数 从主机字节序转换为网络字节序         "Host to Network Short int" 
uint16_t htons(uint16_t hostshort);

// 将 16 位整数 从网络字节序转换为主机字节序。       "Network to Host Short int" 
uint16_t ntohs(uint16_t netshort);

以下是字节序转换的测试例程

cpp 复制代码
int main(int argc, char const *argv[])
{
    //主机端口(小端)
    unsigned short host_port = 0x1234;
    //网络端口(大端)
    unsigned short net_port;
    unsigned long host_addr = 0x12345678;
    unsigned long net_addr;
    net_port = htons(host_port);
    net_addr = htonl(host_addr);
    printf("host_port:%x\n",host_port);
    printf("net_port:%x\n",net_port);
    printf("host_addr:%lx\n",host_addr);    //%lx 长整形16进制
    printf("net_addr:%lx\n",net_addr);
    return 0;
}

2.4.2 网络地址初始化与分配

对于 IP 地址的表示,我们通常很少使用整数型数据来表示地址和端口,而是更倾向于使用点分十进制来表示 IP 地址。接下来介绍的函数都是针对点分十进制表示法的 IP 地址进行操作的,其中一些函数(如 inet_aton 和 inet_pton)还会帮助我们将转换后的地址直接存储在 struct in_addr 结构体中。

cpp 复制代码
int inet_aton(const char *cp, struct in_addr *inp);
// 把IPv4点分十进制表示法的iP主机地址cp转换为二进制形式,
// 并将其存储在struct in_addr 结构体中(一步到位),成功返回1,失败返回0,只适用ip协议

// 等价于inet_aton函数  唯一不同是inet_pton可以使用任意协议
int inet_pton(int af, const char *src, void *dst); 
/*
* af:AF_INET(ipv4)或AF_INET6(ipv6)
* src:包含ip地址字符串的字符数组
* dst:指向一个足够大的缓冲区,用于存储转换后的二进制ip地址
* return:成功返回0,失败返回-1,同时设置errno
*/

char *inet_ntoa(struct in_addr in);
/*
* 返回一个以网络字节序表示的ip地址,由网络号和主机号组成
* in:点分十进制转换成二进制后的结构体(网络字节序)
* return:成功返回0,失败返回-1,同时设置errno
*/

//返回ip地址in的网络号部分,返回值以主机字节序(小端)表示
in_addr_t inet_netof(struct in_addr in);

//返回ip地址in的主机号部分,返回值以主机字节序表示
in_addr_t inet_lnaof(struct in_addr in);

//生成一个IPv4地址,该地址由网络号和网络内的主机号组成
struct in_addr inet_makeaddr(in_addr_t net,in_addr_t host);

注: 和 inet_aton 和 inet_pton 功能类似的函数还有 inet_addr,但是不推荐使用,在某些情况下会出现 Bug,因此,本文章不作介绍。以下是上述函数的测试示例

cpp 复制代码
int main(int argc, char const *argv[])
{
    printf("192.168.6.101的十六进制表示为 0x%X 0x%X 0x%X 0x%X\n",192,168,6,101);
    // 声明结构体接收数据
    struct sockaddr_in server_addr;
    struct in_addr server_in_addr;
    //将结构体里的值清0
    memset(&server_addr,0,sizeof(server_addr));
    memset(&server_in_addr,0,sizeof(server_in_addr));

    //出错返回-1,但是传入结构体的数据不是-1
    inet_aton("192.168.6.101",&server_in_addr);
    printf("inet_aton:0x%X\n",server_in_addr.s_addr);

    //万能方法  inet_pton
    inet_pton(AF_INET,"192.168.6.101",&server_in_addr); 
    printf("inet_pton:0x%X\n",server_in_addr.s_addr);

    //将结构体转换为字符串(主机字节序)
    printf("转化回字符串%s\n",inet_ntoa(server_in_addr));
    //本地网络地址(主机号)
    printf("本地网络地址:0x%X\n",inet_lnaof(server_in_addr));
    //网络号地址
    printf("网络号地址:0x%X\n",inet_netof(server_in_addr));

    //拼接为完整的ip地址
    server_addr.sin_addr = inet_makeaddr(inet_netof(server_in_addr),
                                    inet_lnaof(server_in_addr));
    printf("拼接后的ip地址:%s\n",inet_ntoa(server_addr.sin_addr));
    return 0;
}

2.5 INADDR_ANY

如果我们每次创建服务端套接字都要输 IP 地址会有些繁琐,此时可如下初始化地址信息。

cpp 复制代码
struct sockaddr_in addr;
char*  serv_port = "9190";
memset(&sockaddr_in,0,sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(atoi(serv_port));

利用 常数INADDR_ANY 分配服务器端 IP 地址,若采用这种方式,则可自动获取运行服务器端的计算机IP地址,不必亲自输入。而且,若同一计算机中已分配多个IP地址(多宿主计算机,一般路由器属于这一类),则只要端口号一致,就可以从不同IP地址接收数据。因此,服务器端中优先考虑这种方式。而客户端中除非带有一部分服务器端功能,否则不会采用。

2.6 Socket 编程相关函数

(1)创建套接字 Socket

cpp 复制代码
int socket(int domain, int type, int protocol);
/**
* 创建一个新的Socket。
* domain:指定通信协议族,常使用的协议族如AF_INET(IPv4)或AF_INET6(IPv6)。
* type:指定Socket的类型,即数据传输的方式,
*       如字节流SOCK_STREAM(TCP)或数据段SOCK_DGRAM(UDP)。
* protocol:计算机通信中所使用的协议,通常为0,表示使用默认协议。
* return:成功返回int类型的Socket文件描述符,失败返回-1。
*/

(2)绑定地址 bind

cpp 复制代码
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/**
* 将文件描述符sockfd绑定到一个地址addr
* sockfd:Socket描述符。
* addr:指向要绑定的地址结构的指针。
* addrlen:地址结构的长度。
* return:成功返回0,失败返回-1。
*/

(3)监听连接请求 listen

cpp 复制代码
int listen(int sockfd, int backlog);
/**
* 将sockfd所指向的套接字设置为即监听套接字
* sockfd:Socket描述符。
* backlog:等待连接队列的最大长度。
* return:成功返回0,失败返回-1。
*/

(4)接受连接请求 accept

cpp 复制代码
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/**
* 接受客户端的连接请求。从监听套接字sockfd的待处理连接队列中提取第一个连接请求,
* 创建一个新的连接套接字,并返回指向该套接字的新文件描述符
* 
* sockfd:Socket描述符。
* addr:用于存储 客户端地址信息 的结构体指针。
* addrlen:指向客户端地址结构体长度的指针
* return:成功返回一个新的Socket描述符,新的socket用来和服务端进行连接通信,失败返回-1。
*/

(5)发起连接请求 connect

cpp 复制代码
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/**
* 由客户端调用,来与服务器建立连接
* sockfd:Socket描述符。
* addr:指向服务器端地址结构的指针。
* addrlen:地址结构的长度。
* return:成功返回0,失败返回-1。
*/

(6)发送数据 send

cpp 复制代码
size_t send(int sockfd, const void *buf, size_t len, int flags);
/**
* sockfd:Socket描述符。
* buf:发送缓冲区
* len:要发送的数据长度
* flags:传输控制标志,通常为0,表示不使用特殊行为
* return:功返回发送的字节数,失败返回-1,同时设置errno
*/

(7)接收数据 revc

cpp 复制代码
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
/**
* sockfd:Socket描述符。
* buf:接收缓冲区
* len:接收缓冲区长度
* flags:传输控制标志,通常为0,表示不使用特殊行为
* return:成功返回收到的字节数,如果连接已经正常关闭,返回0
*         如果出现错误,返回-1,同时设置errno
*/

(8)关闭连接 shutdown

cpp 复制代码
int shutdown(int sockfd,int how);
/**
* 关闭socket的一部分或全部连接
* sockfd:Socket描述符。
* how:指定关闭的类型,可以取值为:
* 					SHUT_RD:关闭读
* 					SHUT_WR:关闭写
* 					SHUT_RDWR:同时关闭读取和写入
* return:成功返回0,失败返回-1,同时设置errno
*/

(9)关闭连接 close

cpp 复制代码
int close(int sockfd);
/**
* 终止一个套接字的连接(如果已连接上),然后关闭文件描述符,释放相关的资源。
* close是关闭数据传输的两个方向,之后进程将无法读/写入套接字。
* 
* sockfd:Socket描述符。
* return:成功返回0,失败返回-1,同时设置errno
*/

2.7 C标准中的 main 函数声明

在正式开始套接字编程之前,我们先简要了解一下 C 标准中的 main 函数声明。C 语言提供了以下两种 main 函数声明方式。

(1)无参形式

C99 标准之前,main 函数没有参数的形式被写成 int main()。从C99标准开始,推荐使用 int main(void) 明确指明 main 函数不接受任何参数,以提高代码的可读性和一致性。

cpp 复制代码
int main(void) 
int main()

(2)有参形式

cpp 复制代码
int main(int argc, char *argv[])
  • argc:传递给程序的命令行参数的数量

  • argv:指向字符串数组指针,存储了命令行参数

  • argv[0]:通常是程序名称

  • argv[1] 和 argv[argc-1] 是实际的命令行参数

2.8 套接字应用示例

以下应用示例是基于TCP协议进行通信,目前还未涉及TCP协议,因此,阅读代码时请重点关注套接字相关函数的调用过程,不必理解全部示例。

(1)服务器端 hello_server.c

cpp 复制代码
#define handle_error(cmd,result)    \
    if(result < 0)                  \
    {                               \
        perror(cmd);                \
        return -1;                  \
    }                               \

int main(int argc, char const *argv[])
{
    //服务端和客户端地址
    struct sockaddr_in serv_addr;
    struct sockaddr_in clnt_addr;
    memset(&serv_addr,0,sizeof(serv_addr));
    memset(&clnt_addr,0,sizeof(clnt_addr));
    char message[] = "Hello World";
    //命令行参数是否为两个
    if(argc != 2) {
        printf("Usage:%s <port>\n",argv[0]);
        exit(1);
    }
    //初始化IP地址
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(atoi(argv[1]));
    //INADDR_ANY 是一个宏,表示服务器监听所有可用的网络接口(即 0.0.0.0)
    //serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);   //写法一
    inet_pton(AF_INET,"0.0.0.0",&serv_addr.sin_addr);   //写法二
    //1、创建套接字
    int serv_sock = socket(AF_INET,SOCK_STREAM,0);
    handle_error("socket",serv_sock);
    //2、绑定地址
    int temp=bind(serv_sock,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
    handle_error("bind",temp);
    //3、监听
    temp = listen(serv_sock,128);
    handle_error("listen",temp);
    //4、接受连接
    socklen_t clnt_size = sizeof(clnt_addr);
    //注意accept的第三个参数是 指向服务端地址长度的指针
    //此处的文件描述符clnt_sock是用来向服务端进行通信的
    int clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_addr,&clnt_size);
    handle_error("accept",clnt_sock);
    printf("与客户端%s %d建立连接 文件描述符:%d\n",
                inet_ntoa(clnt_addr.sin_addr),
                ntohs(clnt_addr.sin_port),
                clnt_sock);
    //5、发送数据
    // write(clnt_sock,message,sizeof(message));
    int count = send(clnt_sock,message,1024,0);
    handle_error("send",count);
    //6、关闭套接字连接
    close(serv_sock);
    close(clnt_sock);
    return 0;
}

(2)客户端 hello_client.c

cpp 复制代码
#define handle_error(cmd,result)    \
    if(result < 0)                  \
    {                               \
        perror(cmd);                \
        return -1;                  \
    }                               \

int main(int argc, char const *argv[])
{
    struct sockaddr_in serv_addr;
    struct sockaddr_in clnt_addr;
    memset(&serv_addr,0,sizeof(serv_addr));
    memset(&clnt_addr,0,sizeof(clnt_addr));
    char *message = NULL;
    message = malloc(sizeof(char)*1024);
    if(argc != 3) {
        printf("Usage:%s <IP> <port>\n",argv[0]);
        exit(1);
    }
    serv_addr.sin_family = AF_INET; 
    inet_pton(AF_INET,argv[1],&serv_addr.sin_addr); 
    serv_addr.sin_port = htons(atoi(argv[2])); 
    //客户端套接字通信流程
    int sock = socket(AF_INET,SOCK_STREAM,0);
    handle_error("socket",sock);
    int temp=connect(sock,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
    handle_error("connect",temp);
    printf("连上服务器端%s %d\n",inet_ntoa(serv_addr.sin_addr),
                                                ntohs(serv_addr.sin_port));
    //客户端不必像accept那样返回新的文件描述符,因此一直使用sock通信
    int str_len = recv(sock,message,1024,0);
    handle_error("recv",str_len);
    printf("Message from server:%s\n",message);
    close(sock);
    free(message);
    return 0;
}

(3)测试结果

127.0.0.1是回送地址(loopbackaddress),指的是计算机自身IP地址。因为本例当中服务器端和客户端在同一计算机中运行,因此,连接目标服务器端的地址为127.0.0.1。当然,若用实际IP地址代替此地址也能正常运转。如果服务器端和客户端分别在 2 台计算机中运行,则可以输人服务器端 IP 地址。

相关推荐
暖阳冷月海无涯23 分钟前
数据结构-C语言版本(一)数组
c语言·数据结构
liqingdi4371 小时前
WSL+Ubuntu+miniconda环境配置
linux·windows·ubuntu
luoqice1 小时前
通过 Samba 服务实现 Ubuntu 和 Windows 之间互传文件
linux
Non importa1 小时前
【初阶数据结构】树——二叉树(上)
c语言·数据结构·学习·算法
餘yuqn2 小时前
http 协议与 https 协议的区别
网络协议
汤姆_5113 小时前
【c语言】深度理解指针4——sizeof和strlen
c语言·开发语言
_GR4 小时前
2025年蓝桥杯第十六届C&C++大学B组真题及代码
c语言·数据结构·c++·算法·贪心算法·蓝桥杯·动态规划
2301_780789664 小时前
高防IP如何针对DDoS攻击特点起防护作用
网络协议·tcp/ip·ddos·高防ip·高防cdn
哈哈幸运5 小时前
MySQL运维三部曲初级篇:从零开始打造稳定高效的数据库环境
linux·运维·数据库·mysql·性能优化