目录
[1、IPv4 地址结构 (sockaddr_in)](#1、IPv4 地址结构 (sockaddr_in))
[2、IPv6 地址结构 (sockaddr_in6)](#2、IPv6 地址结构 (sockaddr_in6))
[5、专用 socket 地址结构](#5、专用 socket 地址结构)
[TCP 数据读写:](#TCP 数据读写:)
[UDP 数据读写:](#UDP 数据读写:)
一、主机字节序列和网络字节序列
主机字节序列(Host Byte Order)指的是计算机在内存中存储多字节数据时的顺序。分为大端字节序和小端字节序,不同的主机采用的字节序列可能不同。
- 大端字节序是指一个整数的高位字节存储在内存的低地址处,低位字节存储在内存的高地址处。
- 小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。
在两台使用不同字节序的主机之间传递数据时,可能会出现冲突。所以,在将数据发送到网络时规定整形数据使用大端字节序,所以也把大端字节序成为网络字节序列。对方接收到数据后,可以根据自己的字节序进行转换。
Linux 系统提供如下 4 个函数来完成主机字节序和网络字节序之间的转换:
c
#include <arpa/inet.h>
#include <netinet/in.h>
uint32_t htonl(uint32_t hostlong); // 主机到网络(长整型)长整型的主机字节序转网络字节序
uint16_t htons(uint16_t hostshort); // 主机到网络(短整型)短整形的主机字节序转网络字节序
uint32_t ntohl(uint32_t netlong); // 网络到主机(长整型)长整型的网络字节序转主机字节序
uint16_t ntohs(uint16_t netshort); // 网络到主机(短整型)短整型的网络字节序转主机字节序
不同架构的处理器可能使用不同的字节序。x86架构通常是小端序,而网络协议(如TCP/IP)要求使用大端序。因此,跨平台通信时必须进行字节序转换。
二、套接字地址结构
套接字地址结构用于在网络编程中存储通信所需的地址信息。常见的套接字地址结构包括IPv4、IPv6和Unix域套接字地址结构。
1、IPv4 地址结构 (sockaddr_in)
IPv4套接字地址结构定义在<netinet/in.h>
中,用于存储IPv4地址和端口号。
c
struct sockaddr_in {
sa_family_t sin_family; // 地址族,如AF_INET
in_port_t sin_port; // 16位端口号,网络字节序
struct in_addr sin_addr; // 32位IPv4地址,网络字节序
char sin_zero[8]; // 填充字段,通常置零
};
struct in_addr {
uint32_t s_addr; // IPv4地址,网络字节序
};
2、IPv6 地址结构 (sockaddr_in6)
IPv6套接字地址结构用于存储IPv6地址和端口号。
c
struct sockaddr_in6 {
sa_family_t sin6_family; // 地址族,如AF_INET6
in_port_t sin6_port; // 16位端口号,网络字节序
uint32_t sin6_flowinfo; // 流信息
struct in6_addr sin6_addr; // 128位IPv6地址,网络字节序
uint32_t sin6_scope_id; // 范围ID
};
struct in6_addr {
unsigned char s6_addr[16]; // IPv6地址,网络字节序
};
3、通用套接字地址结构 (sockaddr)
socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,通用套接字地址结构用于在函数参数中传递不同类型的地址结构。其定义如下:
c
struct sockaddr {
sa_family_t sa_family; // 地址族
char sa_data[14]; // 协议地址
};
sa_family 成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族和对应的地址族如下图所示:
4、Unix域套接字地址结构 (sockaddr_un)
Unix域套接字用于本地进程间通信,其地址结构定义在<sys/un.h>
中。
c
struct sockaddr_un {
sa_family_t sun_family; // 地址族,AF_UNIX
char sun_path[108]; // 文件路径名
};
5、专用 socket 地址结构
TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用 socket 地址结构体,它们分别用于 IPV4 和 IPV6:
cpp
/*
sin_family: 地址族 AF_INET
sin_port: 端口号,需要用网络字节序表示
sin_addr: IPV4 地址结构:s_addr 以网络字节序表示 IPV4 地址
*/
struct in_addr{
u_int32_t s_addr;
};
struct sockaddr_in{
sa_family_t sin_family;
u_int16_t sin_port;
struct in_addr sin_addr;
};
struct in6_addr{
unsigned char sa_addr[16]; // IPV6 地址,要用网络字节序表示
};
struct sockaddr_in6{
sa_family_t sin6_family; // 地址族:AF_INET6
u_inet16_t sin6_port; // 端口号:用网络字节序表示
u_int32_t sin6_flowinfo; // 流信息,应设置为 0
struct in6_addr sin6_addr; // IPV6 地址结构体
u_int32_t sin6_scope_id; // scope ID,尚处于试验阶段
};
6、套接字地址结构的转换
在网络编程中,通常需要将人类可读的IP地址和端口号转换为网络字节序的二进制形式。
字符串转二进制地址
使用inet_pton
函数将点分十进制字符串转换为二进制地址。
c
#include <arpa/inet.h>
const char *ip_str = "192.168.1.1";
struct in_addr addr;
inet_pton(AF_INET, ip_str, &addr);
二进制地址转字符串
使用inet_ntop
函数将二进制地址转换为点分十进制字符串。
c
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr, ip_str, INET_ADDRSTRLEN);
7、端口号的转换
端口号需要转换为网络字节序(大端序)后使用。
c
#include <arpa/inet.h>
uint16_t port = 8080;
uint16_t net_port = htons(port); // 主机字节序转网络字节序
uint16_t host_port = ntohs(net_port); // 网络字节序转主机字节序
三、网络编程接口
网络编程接口(API,Application Programming Interface)是不同软件系统间进行通信和数据交换的标准化方式。通过网络API,开发者可以调用远程服务、获取数据或执行特定操作,而无需了解底层实现细节。
socket
socket()创建套接字,成功返回套接字的文件描述符,失败返回-1
domain: 设置套接字的协议簇, AF_UNIX AF_INET AF_INET6 ;type: 设置套接字的服务类型 SOCK_STREAM SOCK_DGRAM ;protocol: 一般设置为 0,表示使用默认协议
cpp
int socket(int domain, int type, int protocol);
bind
bind()将 sockfd 与一个 socket 地址绑定,成功返回 0,失败返回-1
sockfd 是网络套接字描述符 ;addr 是地址结构 ;addrlen 是 socket 地址的长度
cpp
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen
listen()创建一个监听队列以存储待处理的客户连接,成功返回 0,失败返回-1
sockfd 是被监听的 socket 套接字 ;backlog 表示处于完全连接状态的 socket 的上限
cpp
int listen(int sockfd, int backlog);
accept
accept()从 listen 监听队列中接收一个连接,成功返回一个新的连接 socket,该 socket 唯一地标识了被接收的这个连接,失败返回-1
sockfd 是执行过 listen 系统调用的监听 socket ;addr 参数用来获取被接受连接的远端 socket 地址 ;addrlen 指定该 socket 地址的长度
cpp
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
TCP 数据读写:
recv()读取 sockfd 上的数据,buff 和 len 参数分别指定读缓冲区的位置和大小
send()往 socket 上写入数据,buff 和 len 参数分别指定写缓冲区的位置和数据长度
flags 参数为数据收发提供了额外的控制
cpp
ssize_t recv(int sockfd, void *buff, size_t len, int flags);
ssize_t send(int sockfd, const void *buff, size_t len, int flags);
UDP 数据读写:
recvfrom()读取 sockfd 上的数据,buff 和 len 参数分别指定读缓冲区的位置和大小
src_addr 记录发送端的 socket 地址 ;addrlen 指定该地址的长度 ;sendto()往 socket 上写入数据,buff 和 len 参数分别指定写缓冲区的位置和数据长度 ;dest_addr 指定接收数据端的 socket 地址 ;addrlen 指定该地址的长度
cpp
ssize_t recvfrom(int sockfd, void *buff, size_t len, int flags,
struct sockaddr* src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, void *buff, size_t len, int flags,
struct sockaddr* dest_addr, socklen_t addrlen);
close
close()关闭一个连接,实际上就是关闭该连接对应的 socket
cpp
int close(int sockfd);
connect
connect()客户端需要通过此系统调用来主动与服务器建立连接,成功返回 0,失败返回-1
sockfd 参数是由 socket()返回的一个 socket。 serv_addr 是服务器监听的 socket 地址 ;addrlen 则指定这个地址的长度
cpp
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
四、TCP编程流程
TCP 提供的是面向连接的、可靠的、字节流服务。TCP 的服务器端和客户端编程流程如下:

- socket()方法是用来创建一个套接字,有了套接字就可以通过网络进行数据的收发。这也是为什么进行网络通信的程序首先要创建一个套接字。创建套接字时要指定使用的服务类型,使用 TCP 协议选择流式服务(SOCK_STREAM)。
- bind()方法是用来指定套接字使用的 IP 地址和端口。IP 地址就是自己主机的地址,如果主机没有接入网络,测试程序时可以使用回环地址"127.0.0.1"。端口是一个 16 位的整形值,一般 0-1024 为知名端口,如 HTTP 使用的 80 号端口。这类端口一般用户不能随便使用。其次,1024-4096 为保留端口,用户一般也不使用。4096 以上为临时端口,用户可以使用。在Linux 上,1024 以内的端口号,只有 root 用户可以使用。
- listen()方法是用来创建监听队列。监听队列有两种,一个是存放未完成三次握手的连接,一种是存放已完成三次握手的连接。listen()第二个参数就是指定已完成三次握手队列的长度。
- accept()处理存放在 listen 创建的已完成三次握手的队列中的连接。每处理一个连接,则accept()返回该连接对应的套接字描述符。如果该队列为空,则 accept 阻塞。
- connect()方法一般由客户端程序执行,需要指定连接的服务器端的 IP 地址和端口。该方法执行后,会进行三次握手, 建立连接。
- send()方法用来向 TCP 连接的对端发送数据。send()执行成功,只能说明将数据成功写入到发送端的发送缓冲区中,并不能说明数据已经发送到了对端。send()的返回值为实际写入到发送缓冲区中的数据长度。
- recv()方法用来接收 TCP 连接的对端发送来的数据。recv()从本端的接收缓冲区中读取数据,如果接收缓冲区中没有数据,则 recv()方法会阻塞。返回值是实际读到的字节数,如果 recv()返回值为 0, 说明对方已经关闭了 TCP 连接。
- close()方法用来关闭 TCP 连接。此时,会进行四次挥手。
代码示例:
服务端
cpp
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
int main(){
int sockfd = socket(AF_INET,SOCK_STREAM,0);
/*创建套接字,tcp协议
返回值为文件描述符,0标准输入,1标准输出,2标准错误输出,3=sockfd;
参数为地址图、服务类型(tcp为流式服、udp为数据报表)、恒为0
*/
if( sockfd == -1 ){
exit(1);
}
struct sockaddr_in saddr;//定义ipv4专用的地址结构
memset(&saddr,0,sizeof(saddr));//清空,参数为套接字地址,0,套接字大小
saddr.sin_family = AF_INET;//填充地址图
saddr.sin_port = htons(6000);//填充端口,大端,主机字节序列转为网络字节序列,
//1024以内知名端口,root;1024-4096保留端口一般不用;4096以上为临时端口,随便用
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//ip地址,转为无符号整型
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
/*绑定,设置ip和端口
参数为:指定地址的的套接字,地址,地址结构大小
*/
if( res == -1){
printf("bind err\n");
exit(1);
}
res = listen(sockfd,5);
/*创建监听队列,存放客户端发送来的链接请求
参数为:套接字对应的描述符,整数值:监听大小
创建两个监听队列,一个是存放未完成三次握手的连接,一种是存放已完成三次握手的连接。
listen()第二个参数在linux就是指定已完成三次握手队列的长度,在unix上是表示已完成+未完成。
accept()处理已完成三次握手的连接
*/
if( res == -1){
exit(1);
}
while( 1 ){
struct sockaddr_in caddr;//记录客户端地址
socklen_t len = sizeof(caddr);//计算大小
int c = accept(sockfd,(struct sockaddr*)&caddr,&len);
/*接受套接字,阻塞
参数为(接收套接字,存放客户端地址的套接字,指针)
*/
printf("c=%d,ip:%s,port=%d\n",c,inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));
//ip转化为点分十进制的字符串,端口将网络字节序列转化为主机字节序列
while( 1 ){
char buff[128] = {0};
int num = recv(c,buff,127,0);//read
if( num <= 0 ){// n==0 对方关闭,n==-1,失败
break;
}
printf("buff=%s\n",buff);//接收之后,打印
send(c,"ok",2,0);//write,描述符,带发送的数据,大小,标志位
}
printf("client close\n");
close(c);
}
close(sockfd);
exit(0);
}
客户端
cpp
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
int main(){
int sockfd = socket(AF_INET,SOCK_STREAM,0);//创建套接字
if( sockfd == -1){
exit(1);
}
struct sockaddr_in saddr;//ipv4地址
bzero(&saddr, sizeof(saddr)); //memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);//因为服务器用的是6000
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//ip
int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if( res == -1){
printf("connect err\n");
exit(1);
}
while(1){
char buff[128] = {0};
printf("input:\n");
fgets(buff,128,stdin);//从键盘获取数据
if(strncmp(buff,"end",3)==0){
break;
}
send(sockfd,buff,strlen(buff)-1,0);
memset(buff,0,128);
recv(sockfd,buff,127,0);
printf("buff=%s\n",buff);
}
close(sockfd);
exit(0);
}
执行结果:
