3、深入浅出IP地址
① 问题:网络编程接口中一些参数的意义是什么?
????????sock = socket( PF_INET, SOCK_STREAM,0 );
② socket参数详解
- int socket( int domain, int type, int protocol );
????????
- socket()中的domain参数(协议族)
- PF_INET → IPv4互联网协议族
- PF_INET6 → IPv6互联网协议族
- PF_LOCAL → 本地通信的协议族(进程间通信)
- PF_PACKET → 底层数据收发协议族
- PF_IPX → Novell专用协议(互联网分组交换协议)
- 注意:不同协议中的地址表现形式可能不同,网络编程时地址类型必须和协议类型匹配
????????
- socket()中的type和protocol参数
- type 用于指定协议类型
- SOCK_STREAM:流式数据(TCP)
- SOCK_UGRAM:报文式数据(UDP)
- protocol 用于指定协议族中符合类型的具体协议
- domain和type几乎可以唯一确定一种协议,因此,这个参数通常为0
- 即:0 代表domain和type指定后的默认协议
③ 关于端口号和IP地址
- 端口号是一个2字节数据(无符号整数)
- 0-1023作为特定端口被预定义(分配给特定应用程序)
- 一些特定的系统1024-2048也被占用,体现在bind绑定失败,所以在使用时候,尽量使用大一点的端口号
- IP地址是一个4字节地址族(可分为5类地址)(按照网络标识/主机标识的比例)
????????
④ 深入解析IP地址
- IP地址分为网络标识和主机标识两部分
- 网络标识:标识网络主机(设备)所在的网络
- 主机标识:标识网络主机(设备)的具体地址
????????
- 问题:一个IP地址就4个字节,那么如何区分网络标识和主机标识呢?
????????
- IP地址和子网掩码配合使用区分网络标识和主机标识
- 子网掩码的表现形式也是一个4字节的整型数
- 子网掩码用于从IP地址中提取网络标识(按位与)
????????
- 深入理解子网掩码
- 设:子网掩码为M.N.P.Q,则子网可用IP地址n = (256 - M)*(256 - N)*(256 - P)* (256 - Q)
- 例:IP地址211.99.34.33,掩码255.255.255.248。因此:根据上式计算,211.99.34.33所在子网有8个IP地址,根据ip地址与子网掩码与计算,可知 33所在子网地址为211.99.34.32,广播地址为211.99.34.39,子网中可用地址为211.9.34.33 - 211.9.34.38
????????
- IP地址211.99.34.33,掩码255.255.255.248
- 可知211.99.34.33所在子网有8个IP地址,且8=2^3,所以Y = 32-3 = 29
- 表示为211.99.34.33/29(简洁表示法)
- 掩码是32位,这个29代表高位有多少个1
- 算一算
- IP地址192.168.3.44,掩码255.255.255.0,问:子网地址是什么?广播地址是什么?可用地址有多少?简洁表示法是什么?
????????192.168.3.0????????? 192.168.3.255???????? 254个(0广播地址,255子网地址) 通常这个子网下路由器的地址192.168.3.1???????? 192.168.3.44/24
- 特殊的地址
- 0.0.0.0 / 0 保留,常用于代表“缺省网络”
- 127.0.0.0 /8 回环地址,常用于本地软件回送测试
- 255.255.255.255 / 32 广播地址
- 私有地址:不在公网(互联网)使用,只在内网(局域网)使用
- 10.0.0.0 -10.255.255.255 /8
- 172.16.0.0.-172.31.255.255 / 16
- 192.168.0.0 -192.168.255.255 /24
⑤ 网络编程中的地址类型
问题:这样子的强制类型转换不会出问题吗?
????????socket多功能函数,支持不同类型的通信,不同类型的通信中地址的指定是不一样的,所以这个函数的地址类型只能使用sockaddr结构体,
????????但是在具体的编程时,我们才能决定使用哪个专向的结构体。
????????设备根据统一的内部结构体,通过入参sa_family判断具体是哪种类型。
????????
????????
????????注意:inet_ntoa() 重复调用会覆盖上一次的调用; inet_aton() 可以用来检测用户输入的ip是否正确。
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <malloc.h>
int main()
{
unsigned int addr = inet_addr("1.2.3.4");
struct in_addr addr1 = {0x09080706};
struct in_addr addr2 = {0x05040302};
char* s1 = strcpy(malloc(32), inet_ntoa(addr1));
char* s2 = strcpy(malloc(32), inet_ntoa(addr2));// inet_ntoa() 重复调用会覆盖上一次的调用
printf("addr = %x\n", addr);
printf("addr1 = %x\n", addr1.s_addr);
printf("addr2 = %x\n", addr2.s_addr);
printf("s1 = %s\n", s1);
printf("s2 = %s\n", s2);
printf("s1 == s2 : %d\n", s1==s2);
if( inet_aton("D.T.Software", &addr1) )
{
printf("addr1 = %x\n", addr1.s_addr);
}
free(s1);
free(s2);
return 0;
}
????????
4、尝鲜 select 多路复用
① Linux的设计哲学:一切皆文件!!!
② Linux中的文件是什么?
- 狭义
- 文件系统中物理意义上的文件(逻辑上关联的数据集合)
- 广义:
- 设备,管道,内存,。 。 。
- Linux管理的一切对象
③ 理解文件描述符(File Descriptor)
- 文件描述符是一个非负整数值,本质是一个句柄
- 一切对用户(程序员)透明的资源标识都可以看作句柄
- 用户使用文件描述符(句柄)与内核交互
- 内核通过文件描述符操作对应资源的数据结构
④ 一切皆文件的意义
- 统一各种设备的操作方式(open,read, write, close)
- 如:
- IO设备(命令行,显示器)
- 网络设备(网卡)
- ……..
⑤ Linux文件操作编程模式
????????
⑥ 编程实验:以文件方式操作命令行
输入输出设备在系统启动时就打开了。
#include <stdio.h>
#include <unistd.h>
// Linux的设计哲学:一切皆文件!!!
// 输入输出设备也可以当文件来读写
int main()
{
int iofd = 0;
char s[] = "Exp.Joker\n";
int len = 0;
write(0, s, sizeof(s));
len = read(0, s, 5);
s[len] = 0;// 添加结束符
printf("%s\n", s);
return 0;
}
⑦ 事件相关函数的分类
- 阻塞式函数
- 函数调用后需要等待某个事件发生后才会返回(scanf read accept 等数据)
- 非阻塞式函数
- 函数调用后能够及时返回(仅标记等待的事件)
- 事件发生后以回调方式传递
⑧ 阻塞?vs 轮询
- 轮询指依序询问每一个相关设备是否需要服务的方式
- 轮询可用于解决阻塞函数导致程序无法继续执行的问题
????????????????
⑨ 神奇的select()函数
- select()用于监视指定的文件描述符是否产生事件
- 可通过轮询的方式检测目标事件(事件产生则标记发生变化)
- 根据事件类型做出具体处理(如:读取数据)
????????
- 返回值:判断是否有事件发生
- readset 读事件
- writeset 写事件
- timeout 超时(等待时间)
⑩ select()函数的使用步骤
????????
? select()相关数据类型及操作
????????fd_set 数据类型:用来做标记,内部有很多比特位,每个比特位代表一个文件描述符上是否有事件发生。
????????
????????FD_ISSET : 查看关心的位是否发生改变
? 使用select()进行轮询
? 编程实验:select()初体验
#include <sys/select.h>
#include <sys/time.h>
#include <stdio.h>
#include <unistd.h>
int main()
{
int iofd = 0;
char s[] = "Cryin\n";
int len = 0;
fd_set reads = {0}; // 标记对哪些文件描述符感兴趣:数据到达感兴趣
fd_set temps = {0};
struct timeval timeout = {0};// 停留的时间
FD_ZERO(&reads); // 所有标记清零
FD_SET(iofd, &reads);// 标记文件描述符所对应的位置
while( 1 )
{
int r = -1;
temps = reads; // NOTICE!!!
timeout.tv_sec = 0;
timeout.tv_usec = 50000; // 微秒
// [0,1) ,读,写,停留时间
r = select(1, &temps, 0, 0, &timeout);
if( r > 0 )// 如果有事件发生
{
len = read(iofd, s, sizeof(s)-1);
s[len] = 0;// 添加结束符
printf("Input: %s\n", s);
}
else if( r == 0 )// 如果没有事件发生
{
// 计数
static int count = 0;
usleep(10000); // do something else
count++;
if( count > 100 )
{
printf("do something else\n");
count = 0;
}
}
else// 发生错误
{
break;
}
}
return 0;
}
5、基于多路复用的服务端
① 目前服务端的瓶颈分析
????????
?② 解决方案:阻塞变轮询
- 通过select()函数首先监听服务端server_fd,目标事件为“连接”(读)
- 当事件发生(客户端连接)则调用accept()接受连接
- 将client_fd加入监听范围,目标事件为“数据接收”(读)
- 循环查看各个被监听的文件描述符是否有事件发生
③ 实现方式
????????
?④ 实现逻辑
? ? ? ??
?⑤ 实现关键
- 动态调整需要监视的文件描述符
- 当接收到客户端连接时,将客户端文件描述符加入监听变量(fd_set)中
- 当发现客户端断开时,在监听变量(fd_set)中剔除客户端文件描述符
????????????????
- 动态调整需要监视的文件描述符数量
- 保证每个需要监视的文件描述符能够被轮询
- max = (client > max) ? client : max;
⑥ 编程实验:改进后的服务端
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#define JR_SUCCESS 0
#define JR_FAILURE -1
// 新的客户端连接
int server_handler(int server)
{
struct sockaddr_in addr = {0};
socklen_t asize = sizeof(addr);
// 获得用于通信的socket
return accept(server, (struct sockaddr*)&addr, &asize);
}
// 接收客户端的数据
int client_handler(int client)
{
char buf[32] = {0};
int ret = read(client, buf, sizeof(buf)-1);
// 获取到数据,回发
if( ret > 0 )
{
buf[ret] = 0;
printf("Receive: %s\n", buf);
if( strcmp(buf, "quit") != 0 )
{
ret = write(client, buf, ret);
}
else
{
ret = -1;
}
}
return ret;
}
int main()
{
int server = 0;
struct sockaddr_in saddr = {0};
int max = 0;
int num = 0;
fd_set reads = {0}; // 标记对哪些文件描述符感兴趣:数据到达感兴趣
fd_set temps = {0}; // select函数在轮询的时候会影响这个参数,select函数返回的时候,这个参数就已经变了
struct timeval timeout = {0};
// 申请系统资源
server = socket(PF_INET, SOCK_STREAM, 0);
if( server == JR_FAILURE )
{
printf("server socket error\n");
return JR_FAILURE;
}
// 绑定
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = htonl(INADDR_ANY);// "0.0.0.0" 代表本机的连接全部接受
saddr.sin_port = htons(8888);
if( bind(server, (struct sockaddr*)&saddr, sizeof(saddr)) == JR_FAILURE )
{
printf("server bind error\n");
return JR_FAILURE;
}
// 监听
if( listen(server, 1) == JR_FAILURE ) // 每次只服务一个客户端
{
printf("server listen error\n");
return JR_FAILURE;
}
else
printf("server start success\n");
FD_ZERO(&reads); // 所有标记清零
FD_SET(server, &reads); // 标记文件描述符所对应的位置
max = server;
while( 1 )
{
temps = reads;
timeout.tv_sec = 0;
timeout.tv_usec = 10000;
// [0,max] ,读,写,停留时间
num = select(max+1, &temps, 0, 0, &timeout);
// 等待事件发生 (num标记发生事件的数量)
if( num > 0 )
{
int i = 0;
// 遍历查找是否发生服务端事件
for(i=1; i<=max; i++)// [1,max],0代表的是输入输出设备
{
if( FD_ISSET(i, &temps) )
{
// 判断
if( i == server )
{
// 新的客户端连接
int client = server_handler(server);
if( client > -1 )
{
// 标记文件描述符所对应的位置
FD_SET(client, &reads);
// 更新max在select轮询中包含该事件
max = (client > max) ? client : max;
printf("accept client: %d\n", client);
}
}
else
{
int r = client_handler(i);
if( r == -1 )
{
FD_CLR(i, &reads);
close(i);
}
}
}
}
}
}
close(server);
return 0;
}
|