文章目录
- 4.11 socket地址
- 通用 socket地址
- 专用 socket地址
- 4.12 IP地址转换函数(字符串ip -> 整数)
- 4.13TCP通信流程
- 4.14socket函数
4.11 socket地址
socket地址其实是一个结构体,封装端口号和IP等信息。后面的socket相关的api中需要使用到这个socket地址。
客户端 -> 服务器(IP, Port)
通用 socket地址
socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:
#include <bits/socket.h>
struct sockaddr {
sa_family_t sa_family; //表示属于哪一类型的地址/协议
char sa_data[14]; //具体的地址
};
typedef unsigned short int sa_family_t;
sa_family 成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。 (sa_family = PF_UNIX 或者其他的 )
常见的宏 :
协议族(PF: protocol family,也称 domain)和对应的地址族(AF: address family)如下所示:
宏 PF_* 和 AF_* 都定义在 bits/socket.h 头文件中,且后者与前者有完全相同的值,所以二者通常混用。
sa_data 成员用于存放 socket 地址值。但是,不同的协议族的地址值具有不同的含义和长度,如下所示:
由上表可知,14 字节的 sa_data 根本无法容纳多数协议族的地址值。因此,Linux 定义了下面这个新的通用的 socket 地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的。
#include <bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family; //表示协议\地址类型
unsigned long int __ss_align; //用于内存对齐 先不管
char __ss_padding[ 128 - sizeof(__ss_align) ]; //存放具体地址数据
};
typedef unsigned short int sa_family_t;
专用 socket地址
很多网络编程函数诞生早于 IPv4 协议,那时候都使用的是 struct sockaddr 结构体,为了向前兼容,现在sockaddr 退化成了(void *)的作用,传递一个地址给函数,至于这个函数是 sockaddr_in 还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
UNIX 本地域协议族使用如下专用的 socket 地址结构体:
#include <sys/un.h>
struct sockaddr_un
{
sa_family_t sin_family;
char sun_path[108];
};
TCP/IP 协议族有== sockaddr_in== 和 sockaddr_in6 两个专用的 socket 地址结构体,它们分别用于 IPv4 和IPv6:
//最重要
#include <netinet/in.h>
struct sockaddr_in
{
sa_family_t sin_family; /* __SOCKADDR_COMMON(sin_) */
in_port_t sin_port; /* Port number. */ //2字节端口号
struct in_addr sin_addr; /* Internet address. */ //4字节IP地址
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) - sizeof (struct in_addr)]; //这一长条都是剩余部分
};
struct in_addr
{
in_addr_t s_addr;
};
struct sockaddr_in6
{
sa_family_t sin6_family;
in_port_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;
typedef uint32_t in_addr_t;
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。
因为sockaddr只有16字节的char,端口和IP地址分别是short和int,如果要存放的话就要转换成逐个字节单独存放,很麻烦。所以有了sockaddr_in。存放的时候直接调用in_port_t sin_port; struct in_addr sin_addr; 这两个成员,直接赋值就行。
只是!所有相关函数的形参都是sockaddr,所以在调用API的时候需要将sockaddr_in强制转换成 sockaddr。
4.12 IP地址转换函数(字符串ip -> 整数)
字符串ip -> 整数 ,主机、网络字节序的转换
通常,人们习惯用可读性好的字符串来表示 IP 地址,比如用点分十进制字符串表示 IPv4 地址,以及用十六进制字符串表示 IPv6 地址。但编程中我们需要先把它们转化为整数(二进制数)方能使用。
而记录日志时则相反,我们要把整数表示的 IP 地址转化为可读的字符串。
下面 3 个函数可用于用点分十进制字符串表示的 IPv4 地址和用网络字节序整数表示的 IPv4 地址之间的转换:(IPV4 <-> 网络字节序(大端))
//不建议使用 只能在IPV4使用
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);
/*
struct in_addr *inp 其实就是4字节的IP地址
struct in_addr
{
in_addr_t s_addr;
};
typedef uint32_t in_addr_t;
typedef unsigned int uint32_t;
*/
下面这对更新的函数也能完成前面 3 个函数同样的功能,并且它们同时适用 IPv4 地址和 IPv6 地址:
#include <arpa/inet.h>
// p:点分十进制的IP字符串(point), n:表示network,网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
af:地址族: AF_INET AF_INET6
src:需要转换的点分十进制的IP字符串
dst:转换后的结果保存在这个里面
// 将网络字节序的整数,转换成点分十进制的IP地址字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
af:地址族: AF_INET AF_INET6
src: 要转换的ip的整数的地址
dst: 转换成IP地址字符串保存的地方
size:第三个参数的大小(数组的大小)
返回值:返回转换后的数据的地址(字符串),和 dst 是一样的
#include <stdio.h>
#include <arpa/inet.h>
int main() {
// 创建一个ip字符串,点分十进制的IP地址字符串
char buf[] = "192.168.1.4";
unsigned int num = 0;
// 将点分十进制的IP字符串转换成网络字节序的整数
inet_pton(AF_INET, buf, &num);
unsigned char * p = (unsigned char *)#
printf("%d %d %d %d\n", *p, *(p+1), *(p+2), *(p+3));
// 将网络字节序的IP整数转换成点分十进制的IP字符串
//ip地址在整数类型的时候 是四字节
//转换成字符串以后一个数字就是1字节,所以至少15 xxx.xxx.xxx.xxx
//还有最后一个字符串结束符 所以是16
char ip[16] = "";
const char * str = inet_ntop(AF_INET, &num, ip, 16);
printf("str : %s\n", str);
printf("ip : %s\n", str);
printf("%d\n", ip == str);
return 0;
}
显示结果
4.13TCP通信流程
bind()函数绑定服务端的ip和端口号,accept()函数是阻塞的,接受客户端连接请求,创建一个新的文件描述符fd用于读写缓冲区,与客户端进行通信。监听时的是另一个fd专门用来监听。
// TCP 通信的流程
// 服务器端 (被动接受连接的角色)
1. 创建一个用于监听的套接字
- 监听:监听有客户端的连接
- 套接字:这个套接字其实就是一个文件描述符
2. 将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)
- 客户端连接服务器的时候使用的就是这个IP和端口
3. 设置监听,监听的fd开始工作(fd对应着一个读缓冲区和写缓冲区。当内核读缓冲区有数据,就说明被连接了。但这个工作不是程序员监听,是程序自己判断的。)
4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字(fd)
5. 通信
- 接收数据
- 发送数据
6. 通信结束,断开连接
// 客户端
1. 创建一个用于通信的套接字(fd)
2. 连接服务器,需要指定连接的服务器的 IP 和 端口
3. 连接成功了,客户端可以直接和服务器通信
- 接收数据
- 发送数据
4. 通信结束,断开连接
4.14socket函数
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略
int socket(int domain, int type, int protocol);
- 功能:创建一个套接字
- 参数:
- domain: 协议族
AF_INET : ipv4
AF_INET6 : ipv6
AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信)
- type: 通信过程中使用的协议类型
SOCK_STREAM : 流式协议
SOCK_DGRAM : 报式协议
- protocol : 具体的一个协议。一般写0
- SOCK_STREAM : 流式协议默认使用 TCP
- SOCK_DGRAM : 报式协议默认使用 UDP
- 返回值:
- 成功:返回文件描述符,操作的就是内核缓冲区。
- 失败:-1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命
名
- 功能:绑定,将fd 和本地的IP + 端口进行绑定
- 参数:
- sockfd : 通过socket函数得到的文件描述符
- addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息
- addrlen : 第二个参数结构体占的内存大小
- 返回值:
- 成功:返回0
- 失败:-1
int listen(int sockfd, int backlog); // /proc/sys/net/core/somaxconn
- 功能:监听这个socket上的连接
- 参数:
- sockfd : 通过socket()函数得到的文件描述符
- backlog : 未连接的和已经连接的和的最大值, 5
- 返回值:
- 成功:返回0
- 失败:返回-1 并设置错误号
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接
- 参数:
- sockfd : 用于监听的文件描述符
- addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)
- addrlen : 指定第二个参数的对应的内存大小
- 返回值:
- 成功 :用于通信的文件描述符
- -1 : 失败
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能: 客户端连接服务器
- 参数:
- sockfd : 用于通信的文件描述符
- addr : 客户端要连接的服务器的地址信息
- addrlen : 第二个参数的内存大小
- 返回值:成功 0, 失败 -1
ssize_t write(int fd, const void *buf, size_t count); // 写数据
ssize_t read(int fd, void *buf, size_t count); // 读数据
解释:backlog : 未连接的和已经连接的和的最大值, 5
- 未连接队列(Unconnected Queue):这是一种用于存储尚未建立连接的请求或数据包的队列。在网络通信中,未连接队列通常用于临时存储未经身份验证或握手的数据包,等待进一步处理或连接。这些数据包可能需要进一步的验证、身份确认或其他操作,然后才能进入已连接状态。
- 已连接队列(Connected Queue):这是一种用于存储已经建立连接并正在进行通信的请求或数据包的队列。已连接队列中的数据包是已经通过身份验证和握手等步骤,与远程主机建立了连接的数据。这些数据包可以在连接的上下文中进行通信和交换。
这二者总和一般设置成5就够用了
使用命令 cat /proc/sys/net/core/somaxconn 可以看到可以设置的最大值。