文章目录
[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 地址。