Linux高性能服务器编程第五章网络编程基础API
Chapter 5
socket编程
主机字节序和网络字节序
大端字节序:高地址存储在低地址
小端字节序:低地址存储在低地址
查看自己主机是否是小端编址(两种方法,实际上都是看数据在内存中的存储方式)
#include <stdio.h>
void byteorder() {
union
{
short value;
char union_bytes[sizeof(short)];
} byteorder;
byteorder.value = 0x0102;
if ((byteorder.union_bytes[0] == 1) && byteorder.union_bytes[1] == 2) {
printf("big endian\n");
} else if (byteorder.union_bytes[0] == 2 && byteorder.union_bytes[1] == 1) {
printf("little endian\n");
} else {
printf("unknow data format\n");
}
int num = 0x12345678;
// &num 取num的地址, (char*)(&num) 将其强转为char *类型的指针,最后取最低位
char ch = *((char*)(&num));
if (ch == 0x78) {
printf("little endian\n");
} else if (ch == 0x12) {
printf("big endian\n");
} else {
printf("unknow data format\n");
}
}
int main() {
byteorder();
return 0;
}
现在大部分的计算机都是小端字节序,但是仍有部分计算机是使用大端字节序的,所以为了防止出现不同机器字节序导致的数据错乱问题,规定数据传输的时候使用大端字节序,所以如果自己计算机为小端字节序的话,则需要对其进行转换。
#include <sys/socket.h>
// 表示host ot net long host为小端,net为小端
// 转换端口
uint16_t htons(uint16_t hostshort); // 主机字节序 => 网络字节序
uint16_t ntohs(uint16_t netshort); // 主机字节序 => 网络字节序
// 转IP
uint32_t htonl(uint32_t hostlong); // 主机字节序 => 网络字节序
uint32_t ntohl(uint32_t netlong); // 主机字节序 => 网络字节序
#include <arpa/inet.h>
// 将点分十进制字符串的IPV4地址,转换为网络字节序的IPV4地址,失败返回-1
in_addr_t inet_addr(const char* strptr);
// 网络地址与点分十进制相互转换
// 函数返回的是一个静态变量地址值,多次调用会导致被覆盖。
char *inet_ntoa(in_addr _in);
int inet_aton(in_addr* _inp);
一个小小的演示案例
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void big_small() {
int port = 0x1234;
uint16_t port_net = htons(port);
printf("origin port : 0x%x, net port : 0x%x\n", port, port_net);
in_addr addr1, addr2;
uint32_t l1, l2;
l1 = inet_addr("127.0.0.1");
l2 = inet_addr("192.168.126.126");
memcpy(&addr1, &l1, sizeof addr1);
memcpy(&addr2, &l2, sizeof addr2);
// inet_ntoa返回的是静态字符串,多次调用会被覆盖
printf("l1 = %s , l2 = %s\n", inet_ntoa(addr1), inet_ntoa(addr2));
}
int main() {
big_small();
return 0;
}
输出为:
origin port : 0x1234, net port : 0x3412
l1 = 127.0.0.1 , l2 = 127.0.0.1
为什么 l1 和 l2 都是127.0.0.1
呢?在printf() 调用的参数是从右边往左边执行的,以为addr1的地址将addr2
的地址覆盖了,所以我们才看到 l1
和 l2
都是 127.0.0.1
socket 简介
- socket地址其实是一个
结构体
,封装端口号和IP等信息 - 后面的socket相关的api中需要使用到这个 socket地址
通用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
成员是地址族类型(sa_family_t
)的变量地址族类型
通常与协议族类型
对应- 宏
PF_*
和AF_*
都定义在bits/socket.h
或sys/socket.h
头文件中,且后者与前者有完全相同的值,所以二者通常混用
协议族 地址族 描述 PF_UNIX AF_UNIX UNIX本地域协议族 PF_INET AF_INET TCP/IPv4协议族 PF_INET6 AF_INET6 TCP/IPv6协议族 -
sa_data
sa_data
成员用于存放 socket 地址值,不同的协议族的地址值具有不同的含义和长度
协议族 地址值含义和长度 PF_UNIX 文件的路径名,长度可达到108字节 PF_INET 16 bit 端口号和 32 bit IPv4 地址,共 6 字节 PF_INET6 16 bit 端口号,32 bit 流标识,128 bit IPv6 地址,32 bit 范围 ID,共 26 字节 -
由上表可知,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
,由地址族确定,然后函数内部再强制类型转化为所需的地址类型 -
不同socket地址对比图
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tZPVpM5t-1665396573314)(D:\Document\学习笔记\pics\sock.png)]
所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可)
(struct sockaddr*) sockaddr_in
,因为所有 socket 编程接口使用的地址参数类型都是 sockaddr
UNIX 本地域协议族
#include <sys/un.h>
struct sockaddr_un {
sa_family_t sin_family;
char sun_path[108];
};
TCP/IP协议族
// IPV4
#include <netinet/in.h>
struct sockaddr_in {
sa_family_t sin_family; /* __SOCKADDR_COMMON(sin_) */
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* 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;
};
// IPV6
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))
相关函数
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // 包含了这个头文件,上面两个就可以省略(因为已经包含上面两个)int socket(int domain, int type, int protocol);
int socket(int domain, int type, int protocol);
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
int socket(int domain, int type, int protocol);
- 功能:创建一个套接字
- 参数:
domain
:协议族(常用如下)AF_INET
:ipv4
AF_INET
6 :ipv6
AF_UNIX
,AF_LOCAL
:本地套接字通信(进程间通信)
type
:通信过程中使用的协议类型SOCK_STREAM
: 流式协议SOCK_DGRAM
: 报式协议
protocol
:具体的一个协议,一般写0,用于指定type参数的默认协议类型SOCK_STREAM
: 流式协议默认使用 TCPSOCK_DGRAM
: 报式协议默认使用 UDP
- 返回值
- 成功:返回文件描述符,操作的就是内核缓冲区
- 失败:-1
-
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能:绑定,将fd 和本地的IP和端口进行绑定
- 参数:
sockfd
:通过socket函数得到的文件描述符addr
:需要绑定的socket地址,这个地址封装了本地的ip和端口号的信息addrlen
:第二个参数结构体占的内存大小
- 返回值:成功:0,失败:-1
-
int listen(int sockfd, int backlog);
-
功能:监听这个socket上的连接
-
参数:
-
sockfd
:通过socket()函数得到的文件描述符 -
backlog
:未连接的和已经连接的和的最大值,可用cat /proc/sys/net/core/somaxconn
查看Linux设置值,一般指定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
通信流程
-
服务器端(被动接收连接)
-
创建一个用于监听的套接字
- 监听:监听有客户端的连接
- 套接字:这个套接字其实就是一个文件描述符
-
将这个
监听文件描述符
和本地的IP和端口绑定(IP和端口就是服务器的地址信息)- 客户端连接服务器的时候使用的就是这个IP和端口
-
设置监听,
监听的fd
开始工作 -
阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个
和客户端通信的套接字(fd)
-
通信
- 接收数据
- 发送数据
-
通信结束,断开连接
-
-
客户端
-
创建一个用于通信的套接字(fd)
-
连接服务器,需要指定连接的服务器的 IP 和 端口
-
连接成功了,客户端可以直接和服务器通信
- 接收数据
- 发送数据
-
通信结束,断开连接
-
TCP通信实例
服务器端
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <netinet/in.h>
#define SERVERIP "127.0.0.1"
#define PORT 8888
#define MAXLEN 4096
int main() {
// 1. 创建用于监听的套接字
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(listenfd != -1);
// 2. 绑定IP、端口
sockaddr_in server_addr;
// 初始化为0
bzero(&server_addr, sizeof server_addr);
server_addr.sin_family = AF_INET;
// 点分十进制转化为网络字节序
inet_pton(AF_INET, SERVERIP, &server_addr.sin_addr);
// host to network port
server_addr.sin_port = htons(PORT);
int ret = bind(listenfd, (sockaddr*)&server_addr, sizeof server_addr);
assert(ret != -1);
// 3. 监听接口
ret = listen(listenfd, 8);
assert(ret != -1);
// 4. 接受客户端连接(被动)
sockaddr_in client_addr;
socklen_t client_addr_len= sizeof client_addr;
int clientfd = accept(listenfd, (sockaddr*)&client_addr, &client_addr_len);
// 输出客户端信息
char client_ip[16] = {0};
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, sizeof client_ip);
uint16_t client_port = ntohs(client_addr.sin_port);
printf("client ip : %s, port : %d\n", client_ip, client_port);
// 5. 开始通信交换数据
char buf[MAXLEN] = {0};
while(true) {
ret = read(clientfd, buf, sizeof buf);
// printf("ret: %d", ret);
if (ret == -1) {
perror("recv");
exit(-1);
} else if (ret == 0) {
printf("客户端关闭连接...\n");
} else if (ret > 0) {
printf("CLINET %s > %s\n", client_ip, buf);
}
char *send_msg = "Welcome, I am server.";
write(clientfd, send_msg, strlen(send_msg) + 1);
}
// 关闭文件描述符
close(clientfd);
close(listenfd);
return 0;
}
客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#define SERVERIP "127.0.0.1"
#define PORT 8888
#define MAXLEN 4096
int main () {
// 1. 创建连接套接字
int connfd = socket(AF_INET, SOCK_STREAM, 0);
assert(connfd >= 0);
// 2. 连接服务器
sockaddr_in server_addr;
bzero(&server_addr, sizeof server_addr);
server_addr.sin_family = AF_INET;
inet_pton(AF_INET, SERVERIP, &server_addr.sin_addr);
server_addr.sin_port = htons(PORT);
int ret = connect(connfd, (sockaddr*)&server_addr, sizeof server_addr);
assert(ret >= 0);
// 3. 与服务器通信,交换数据
char buf[MAXLEN] = {0};
while(true) {
char *send_msg = "I am client, Hi\n";
// sizeof遇到空格会终止
write(connfd, buf, strlen(buf) + 1);
sleep(1);
ret = read(connfd, buf, sizeof buf);
if (ret == -1) {
perror("read");
exit(-1);
} else if (ret == 0) {
printf("服务器断开连接...\n");
break;
} else {
printf("SERVER >%s\n", buf);
}
}
close(connfd);
return 0;
}
输出
服务器端
客户端
这里发现了一个有意思的现象,client
第一次输出的为空,根据TCP三次握手我们可以得知,在client establish
之后又发了一次空的数据包,所以第一次输出是空。(此处解释可能有误,如有正解,请指出)