一、socket的四种IO模型
1、阻塞型
最常用/最简单/效率低
函数本身不具备阻塞属性,而是由于文件描述符本身导致函数阻塞。
在默认情况下Linux建立的socket套接都是阻塞的
2、非阻塞
可以设置进程不阻塞在IO操作上,需要轮询
占用CPU资源较大
3、多路复用
同时对多个IO进行操作
可以设置在规定的时间内检测数据是否到达
4、信号驱动型IO
属于一步通信方式
当socket中有数据到达时,通过发送信号告知用户
二、阻塞性IO
1、读阻塞
当套接字接收缓冲区中没有数据可以读取时调用 如 read/recv/recvfrom就会导致阻塞
当有数据到达时,内核便会去唤醒进程,通过read等函数来访问数据
如果进程阻塞过程中意外,那么进程将永远阻塞下去。
2、写阻塞
发生写阻塞的机会比较少,一般出现在写缓冲区无法写入即将写入的数据时
当无法写入数据时便会进入阻塞等待
一旦发送的缓冲区拥有足够的空间,则内核会唤醒对应的进程进行写入操作
而UDP协议中并不存在发送缓冲区满的情况,UDP套接字执行写操作时永远不会发生阻塞。
三、非阻塞IO
如果有一个IO操作不能马上完成则系统则会让我们的进程进入睡眠状态等待
当我们将一个套接字设置为非阻塞模式时,则系统不会让我们的进程进入睡眠等待而是直接返回错误
当一个程序使用非阻塞模式的套接字,他需要使用循环来不断检查文件描述如是否有数据可读
应用程序不停循环判断将会消耗非常大的CPU资源,一般不推荐使用
四、非阻塞的实现方法
当我们一开始建立一个套接字描述符时,系统内核会默认设置为阻塞型IO,我们可以使用函数 来设置套接字为非阻塞状态。
1、fcntl(文件描述词操作)
fcntl ( 文件描述词操作 )
头文件:
#include <unistd.h>
#include <fcntl.h>
定义函数 :
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock * lock);
参数分析:
fd --> 需要设置的文件描述符
cmd --> 设置的功能
F_DUPFD 用来查找大于或等于参数 arg 的最小且仍未使用的文件描述词, 并且复制参数 fd 的
F_GETFD 取得 close-on-exec 旗标. 若此旗标的 FD_CLOEXEC 位为 0, 代表在调用 exec(
F_SETFD 设置 close-on-exec 旗标. 该旗标以参数 arg 的 FD_CLOEXEC 位决定.
F_GETFL 取得文件描述词状态旗标, 此旗标为 open()的参数 flags.
F_SETFL 设置文件描述词状态旗标, 参数 arg 为新旗标, 但只允许 O_APPEND、O_NONBLOCK
F_GETLK 取得文件锁定的状态.
F_SETLK 设置文件锁定的状态. 此时 flcok 结构的 l_type 值必须是 F_RDLCK、F_WRLCK 或
F_UNLCK. 如果无法建立锁定, 则返回-1, 错误代码为 EACCES 或 EAGAIN.
F_SETLKW 同 F_SETLK 作用相同, 但是无法建立锁定时, 此调用会一直等到锁定动作成功为止
返回值:成功返回0,失败返回-1
2、结构体的定义
struct flcok
{
short int l_type; //锁定的状态
short int l_whence; //决定 l_start 位置
off_t l_start; //锁定区域的开头位置
off_t l_len; //锁定区域的大小
pid_t l_pid; //锁定动作的进程
};
l_type 有三种状态:
F_RDLCK 建立一个供读取用的锁定
F_WRLCK 建立一个供写入用的锁定
F_UNLCK 删除之前建立的锁定
l_whence 也有三种方式:
SEEK_SET 以文件开头为锁定的起始位置.
SEEK_CUR 以目前文件读写位置为锁定的起始位置
SEEK_END 以文件结尾为锁定的起始位置.
3、操作的例子
int socket_fd = socket(...........); // 创建一个套接字描述符
int state = fcntl(socket_fd , F_GETFL , 0) ; // 获得当前描述符的旗标
state |= O_NONBLOCK ; // 在原基础上增加非阻塞属性
fcntl(scoket_fd , F_SETFL , state ); // 把配置的好的旗标重新设置回描述符中
五、用非阻塞的形式写一个服务器
使用非阻塞的IO来实现一个服务器,可以接收都哦个客户端的连接(最多20个客户端),并在收到消息后打印消息内容。
非阻塞的代码
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h> // 包含了地址结构体的定义声明
#include <netinet/in.h>
#include <pthread.h>
#include <stdbool.h>
#include <fcntl.h>
// 设置一个结构体
typedef struct clinet
{
int fd[20] ; // 用来保存每一个在线的用户的描述符
int num ; // 记录当前在线人数
}Clinet ;
int add_cli(Clinet * P_cli ,int connect_fd)
{
if ( P_cli->num >= 20 )
{
return -1 ;
}
// 把连接的套接字先设置为非阻塞状态
int state = fcntl(connect_fd , F_GETFL); // 获取 描述符connect_fd 的属性
state |= O_NONBLOCK ; // 设置属性信息, 添加上非阻塞状态
fcntl(connect_fd , F_SETFL ,state ); // 把配置好的属性,设置回到 connect_fd 描述符中
(P_cli->fd)[P_cli->num] = connect_fd ; // 把新连接上来的客户端的描述符保存下来
P_cli->num ++ ; // 数组下标自加
return 0 ;
}
int main(int argc, char const *argv[])
{
// 创建一个套接字 (购买一台手机)
int sock_fd = socket( AF_INET , SOCK_STREAM , 0 ); // 使用IPV4协议簇, 流式套接字(TCP)
if (-1 == sock_fd)
{
perror("socket rror");
return -1 ;
}
// 配置自己的IP和端口号等信息
// struct sockaddr_in // IPV4地址结构体
// {
// u_short sin_family;// 地址族
// u_short sin_port;// 端口
// struct in_addr sin_addr;// IPV4 地址
// char sin_zero[8];
// };
int addrlen = sizeof(struct sockaddr_in);
struct sockaddr_in my_addr = {
.sin_addr.s_addr = htonl(INADDR_ANY) , // 设置服务器的地址, INADDR_ANY 指本机中任何一个地址
.sin_family = AF_INET , // 使用 IPV4 协议簇
.sin_port = htons(65000) // 设置服务器的端口号为 65000
};
// 绑定地址信息 (IP + 端口号 + 协议簇)
int ret_val = bind( sock_fd, (struct sockaddr *)&my_addr, addrlen);
if (ret_val == -1 )
{
perror("bind error");
return -1 ;
}else{
printf(" bind succeed !!\n") ;
}
// 设置监听 (打开铃声)
if(listen( sock_fd, 5 )) // 设置sock_fd 为监听套接字, 同时可以接收连接请求数为 5 + 4(默认值)
{
perror("listen error");
return -1 ;
}
// 设置监听套接字为非阻塞属性
int state ;
state = fcntl(sock_fd , F_GETFL); // 获取 描述符sock_fd 的属性
state |= O_NONBLOCK ; // 设置属性信息, 添加上非阻塞状态
fcntl(sock_fd , F_SETFL ,state ); // 把配置好的属性,设置回到 sock_fd 描述符中
int connect_fd = -1 ;
Clinet * P_cli = calloc(1, sizeof(Clinet));
char * msg = calloc(1,128);
while(1)
{
// 等待连接
struct sockaddr_in from_addr; // 由于前面已经 把 监听套接字sock_fd 设置为非阻塞状态
connect_fd = accept( sock_fd , (struct sockaddr *)&from_addr, &addrlen);
//由于是非阻塞状态,所以一开始为-1是正常的,-1后要重新来
if ( connect_fd > 0) // 如果描述符大于0 则表示有客户端连接
{
printf("新客户端:%d 连接成功!!\n" , connect_fd);
add_cli(P_cli , connect_fd);
}
// 轮询是否有数据到达
for (int i = 0; i < P_cli->num ; i++)
{
// printf("LINE:%d\n" , __LINE__);
bzero(msg , 128) ;
// recv 的返回值与 read 返回值一致, 成功则返回实际读取的字节数
ret_val = recv((P_cli->fd)[i] , msg , 128 , 0 );
if (ret_val > 0)
{
printf("from %d , msg : %s" , P_cli->fd[i] , msg );
}
}
}
return 0 ;
}