前面介绍的函数,如:recv、send、read、write都是阻塞性函数,如果资源没有准备好,调用该函数的进程将进入阻塞状态。
本节将介绍两种多路复用的解决方案。
fcntl用来操作文件描述符(如套接字,套接字是抽象出来的概念,本质上也是文件描述符)的一些函数,
参数是fd(套接字描述符)和cmd(操作)。
主控线程:产生fd,维护一个fd的动态数组
子线程:遍历动态数组中所有fd,并通过这写fd和对应的客户端进行双向通信(采用非阻塞read/write)
#ifndef __VECTOR_H__
#define __VECTOR_H__
typedef struct{
int *fd;
int counter;
int max_counter;
}VectorFD;
extern VectorFD* create_vector_fd(void);//创建数组
extern void destroy_vector_fd(VectorFD *);//销毁数组
extern int get_fd(VectorFD *, int index);//根据下标获得某个套接字描述符
extern void remove_fd(VectorFD *, int fd);//移除某个套接字描述符
extern void add_fd(VectorFD *, int fd);//增加某个套接字描述符
#endif
#include
#include
#include
#include
#include "vector_fd.h"
static void encapacity(VectorFD *vfd)
{
if(vfd->counter >= vfd->max_counter){
int *fds = (int*)calloc(vfd->counter + 5, sizeof(int));
assert(vfd != NULL);
memcpy(fds, vfd->fd, sizeof(int) * vfd->counter);
free(vfd->fd);
vfd->fd = fds;
vfd->max_counter += 5;
}
}
static int indexof(VectorFD *vfd, int fd)
{
int i = 0;
for(; i < vfd->counter; i++)
{
if(vfd->fd[i] == fd) return i;
}
return -1;
}
VectorFD* create_vector_fd(void)
{
VectorFD *vfd = (VectorFD*)calloc(1,sizeof(VectorFD));//创建一个VectorFD结构体,对结构体成员fd赋为一个动态数组
assert(vfd != NULL);
vfd->fd = (int*)calloc(5, sizeof(int));//初始动态数组中可以存放5个socket描述符
assert(vfd->fd != NULL);
vfd->counter = 0;
vfd->max_counter = 0;
return vfd;
}
void destroy_vector_fd(VectorFD *vfd)
{
assert(vfd != NULL);
free(vfd->fd);
free(vfd);
}
int get_fd(VectorFD *vfd, int index)
{
assert(vfd != NULL);
if(index < 0 || index > vfd->counter-1)
return 0;
return vfd->fd[index];
}
void remove_fd(VectorFD *vfd, int fd)
{
assert(vfd != NULL);
int index = indexof(vfd, fd);
if(index == -1) return;
int i = index;
for(; i < vfd->counter-1; i++){
vfd->fd[i] = vfd->fd[i+1];
}
vfd->counter--;
}
void add_fd(VectorFD *vfd, int fd)
{
assert(vfd != NULL);
encapacity(vfd);//扩展动态数组
vfd->fd[vfd->counter++] = fd;
}
编译运行:编译此文件需要包含-Iinclude——增加头文件的搜索路径
gcc -o obj/vector_fd.o -Iinclude -c src/vector_fd.c
在TCP多线程基础上进行修改,改多线程的阻塞性读写为非阻塞性——
1) 不再采用自定义发送接收message的协议,只采用标准的read/write
2) 包含#include "vector_fd.h"头文件
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "vector_fd.h"
//#define INADDR_ANY (uint32_t)0x00000000
VectorFD *vfd;
int sockfd;
void sig_handler(int signo)
{
if(signo == SIGINT){
printf("server close\n");
/*step 6 close socket*/
close(sockfd);
//销毁动态数组
destroy_vector_fd(vfd);
exit(1);//0 or 1 all can close
}
}
/*
fd对应于某个连接的客户端
和某一个连接的客户端进行双向通信(非阻塞方式)
*/
void do_service(int fd)
{
char buff[512];
memset(buff, 0, sizeof(buff));
//因为采用非阻塞方式,若读不到数据直接返回,直接服务下一个客户端,所以不用判断size小于0的情况
size_t size = read(fd, buff, sizeof(buff));
if(size == 0){//客户端已经关闭连接
//不要用标准IO来输出,因为标准IO带缓存功能可能会出现问题,建议用内核提供的read/write函数往终端输出信息
char info[] = "client closed";
write(STDOUT_FILENO, info, sizeof(info));
//从动态数组中删除对应的fd
remove_fd(vfd, fd);
//关闭对应客户端的socket
close(fd);
}
else if(size > 0){
write(STDOUT_FILENO, buff, sizeof(buff));
if(write(fd, buff, size) < 0){//客户端关闭连接,产生EPIPE,跳出进程
if(errno == EPIPE){
perror("write error");
remove_fd(vfd, fd);
close(fd);
}
}
}
}
void out_addr(struct sockaddr_in *clientaddr)
{
char ip[16];
memset(ip, 0, sizeof(ip));
int port = ntohs(clientaddr->sin_port);
inet_ntop(AF_INET, &clientaddr->sin_addr.s_addr, ip, sizeof(ip));
printf("%s(%d) connected!\n", ip, port);
}
//定义线程运行函数,子线程不断地遍历动态数组,然后与客户端进行非阻塞读写操作
void* th_fn(void *arg)
{
int i;
while(1){
i = 0;
//遍历动态数组中的socket描述符
for(; i < vfd->counter; i++){
do_service(get_fd(vfd, i));
}
}
return (void*)0;
}
int main(int argc, char *argv[])
{
if(argc < 2){
printf("usage: %s #port\n", argv[0]);//打印程序的名字(包括路径)
exit(1);
}
if(signal(SIGINT, sig_handler) == SIG_ERR){//SIGINT:ctrl+C can end ; signal function get SIG_ERR if error
perror("signal sigint error"); //perror function is defined in ,we can see" str: error message"
exit(1);
}
/*step 1 creat socket
*socket is a struct in kernel
*AF_INET: IPV4
*SOCK_STREAM: tcp protocol(udp: SOCK_DGRAM)
**/
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0){
perror("socket error");
exit(1);
}
/*step 2 bind()
*bound with socket and address(ip\port\intnet type)
*sockaddr_in is special net_struct for internet */
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr)); //clear
serveraddr.sin_family = AF_INET; //IPV4(Host byte order is ok)
serveraddr.sin_port = htons(atoi(argv[1]));//port from terminal(atoi:string to int)(htons:Host to Network byte order 16bit)
serveraddr.sin_addr.s_addr = INADDR_ANY; //htons("192.168.0.10")
if(bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) < 0){//cast as common address type : sockaddr
perror("bind error");
exit(1);
}
/*step 3 listen()
*tell system to accept connecting request (in server port)
*put connecting request to queue* (10 is the length of queue)
*/
if(listen(sockfd, 10) < 0){
perror("listen error");
exit(1);
}
/*step 4 accept()
*get a connection and return the new socket file descriptor(sockfd)
*This sockfd(client's fd) is different from the sockfd in step 1(server's fd)
*/
//创建放置套接字描述符fd的动态数组
vfd = create_vector_fd();
//设置线程的分离属性
pthread_t th;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
int err;
if((err = pthread_create(&th, &attr, th_fn, (void*)0)) != 0){
perror("pthread create error");
exit(1);
}
pthread_attr_destroy(&attr);
/* 1)主控线程负责去调用accept获得客户端的连接,将新的socket描述符放置到动态数组中
* 2)启动的子线程负责遍历动态数组中socket描述符并和对应的客户端进行双向通信(采用非阻塞读写)
*/
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);//地址结构体,用于放置客户端地址信息
while(1){
int fd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
if(fd < 0) {
perror("accept error");
continue;
}
out_addr(&clientaddr);
//将读写修改为非阻塞方式 fcntl
int val;
fcntl(fd, F_GETFL, &val);//通过F_GETFL宏命令获得原来套接字对应的状态标志,放到对应的val里
val |= O_NONBLOCK;
fcntl(fd, F_SETFL, val);//非阻塞方式
//将返回新的socket描述符加入到动态数组中
add_fd(vfd, fd);
}
return 0;
}
编译运行:编译此文件需要包含-Iinclude——增加头文件的搜索路径
pthread是动态库,需要用-lpthread在链接阶段链接pthread库,所有的动态库都需要用-lxxx来引用
gcc -o bin/echo_tcp_server_fcntl -Iinclude obj/vector_fd.o src/echo_tcp_server_fcntl.c -lpthread
#include
#include
#include
int select (int maxfdp1, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
返回:准备就绪的描述符数,若超时则为0,若出错则为-1
struct timeval{//设置超时
long tv_sec;//秒
long tv_usec;//微秒
};
maxfdp1: 最大fd加1,在三个描述符集中找到最高描述符编号值+1, 作为函数的第一个参数
readfds、writefds、exceptfds(不常用):指向描述符集的指针。这三个描述符集说明了我们关心的可读、可写和处于异常条件的各个描述符。每个描述符集存放在一个fd_set数据类型中。
timeout:指定愿意等待的时间,有三种值:NULL(永远等待,直到捕捉到信号或文件描述符已经准备好);具体值(struct timeval类型的指针,等待timeout时间还没有文件描述符准备好,就立刻返回);0(从不等待,测试所有指定的描述符并立刻返回)
select()用来等待文件描述符状态的改变。参数maxfdp1代表最大的文件描述符加1,参数readfds、writefds和exceptfds 称为描述符集,是用来回传该描述符的读,写或异常状况。
用户调用select()函数会产生阻塞,委托内核检查传进去的描述符集是否可以使用,返回的是准备就绪的描述符个数,超过规定的检查时间返回0,出错返回-1。
告诉内核:我们所关心的描述符;对于每个描述符所关心的条件(可读?可写?异常情况?);希望等待多长时间
select返回:已经准备好的描述符数量;哪个描述符已经准备好了条件;使用这种返回值,可以调用相应的IO函数,并确知函数不会阻塞
select函数根据希望进行的文件操作,对文件描述符分类处理,
文件描述符的处理主要涉及了四个宏函数:
FD_ZERO(fd_set *set);清楚一个文件描述符集
FD_SET(int fd,fd_set*set);将一个文件描述符加入到某描述符集中
FD_CLR(int fd,fd_set* set);将一个文件描述符从某个某描述符集中清除
FD_ISSET(int fd,fd_set *set);测试描述符集set中相关fd的位是否有变化
使用select函数前,要用FD_ZERO和FD_SET来初始化文件描述符集;
使用select函数时,可循环使用FD_ISSET测试描述符集
在执行完成对相关的文件描述符的操作后,要使用FD_CLD来清空描述符集。
#include ...
//#define INADDR_ANY (uint32_t)0x00000000
VectorFD *vfd;
int sockfd;
void sig_handler(int signo)
{}
/*
fd对应于某个连接的客户端
和某一个连接的客户端进行双向通信(非阻塞方式)
*/
void do_service(int fd)
{
char buff[512];
memset(buff, 0, sizeof(buff));
//因为采用非阻塞方式,若读不到数据直接返回,直接服务下一个客户端,所以不用判断size小于0的情况
size_t size = read(fd, buff, sizeof(buff));
if(size == 0){//客户端已经关闭连接
char info[] = "client closed";
write(STDOUT_FILENO, info, sizeof(info));
//从动态数组中删除对应的fd
remove_fd(vfd, fd);
//关闭对应客户端的socket
close(fd);
}
else if(size > 0){
printf("%s\n", buff);
if(write(fd, buff, size) < 0){//客户端关闭连接,产生EPIPE,跳出进程
if(errno == EPIPE){
perror("write error");
remove_fd(vfd, fd);
close(fd);
}
}
}
}
void out_addr(struct sockaddr_in *clientaddr)
{}
//遍历出动态数组中所有的描述符,并加入到描述符集set中
//同时此函数返回动态数组中最大的描述符标号
int add_set(fd_set *set)
{
FD_ZERO(set);
int max_fd = vfd->fd[0];
int i = 0;
for(; i < vfd->counter; i++){
int fd = get_fd(vfd, i);
if(fd > max_fd) max_fd = fd;
FD_SET(fd, set);//将fd加入到描述符集中
}
return max_fd;
}
//定义线程运行函数,子线程不断地遍历动态数组,然后与客户端进行非阻塞读写操作
void* th_fn(void *arg)
{
struct timeval t;//超时时间 2s
t.tv_sec = 2;
t.tv_usec = 0;
int n = 0;
int maxfd;
fd_set set;//描述符集
maxfd = add_set(&set);
//调用select函数会阻塞,委托内核检查传入的描述符是否准备好,若有则返回准备好的描述符数量,超时返回0
//第一个参数是描述符的范围(最大描述符+1)
while((n = select(maxfd+1, &set, NULL, NULL, &t)) >= 0){
if(n > 0){
//检测哪些描述符准备好,并和准备好的描述符对应客户端进行双向通信
int i = 0;
for(; i < vfd->counter; i++){
int fd = get_fd(vfd, i);
if(FD_ISSET(fd, &set))
do_service(fd);
}
}
//重新设置时间,清空描述符集
t.tv_sec = 2;
t.tv_usec = 0;
//重新遍历动态数组中最新描述符放置到描述符集中
maxfd = add_set(&set);
}
return (void*)0;
}
int main(int argc, char *argv[])
{
...
/* 1)主控线程负责去调用accept获得客户端的连接,将新的socket描述符放置到动态数组中
* 2) a) 启动的子线程调用select函数委托内核去检查传入到select中的描述符是否准备好,并和对应的客户端进行双向通信(非阻塞)\
* b) 利用FD_ISSET来找出准备好的那些描述符,并和对应的客户端进行双向通信
*/
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);//地址结构体,用于放置客户端地址信息
while(1){
int fd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
if(fd < 0) {
perror("accept error");
continue;
}
out_addr(&clientaddr);
//将返回新的socket描述符加入到动态数组中
add_fd(vfd, fd);
}
return 0;
}
编译运行:编译此文件需要包含-Iinclude——增加头文件的搜索路径
pthread是动态库,需要用-lpthread在链接阶段链接pthread库,所有的动态库都需要用-lxxx来引用