(1)在默认的阻塞模式下的套接字里,recv会阻塞在那里,直到套接字连接上有数据可读,把数据读到buffer里后recv函数才会返回,不然就会一直阻塞在那里。在单线程的程序里出现这种情况会导致主线程(单线程程序里只有一个默认的主线程)被阻塞,这样整个程序被锁死在这里,如果永 远没数据发送过来,那么程序就会被永远锁死。这个问题可以用多线程解决,但是在有多个套接字连接的情况下,这不是一个好的选择,扩展性很差。
(2)在非阻塞模式的套接字里,recv的调用不管套接字连接上有没有数据可以接收都会马上返回。但在没有数据的情况下,recv确实是马上返回了,但是也返回了一个错误:WSAEWOULDBLOCK,即请求的操作没有成功完成。
若重复调用recv并检查返回值,直到成功为止,但是这样做效率很成问题,开销太大。
select就是为了解决上述两个问题,能够实现在一个线程内完成并发操作,即I/O多路复用:多个描述符的I/O操作都能在一个线程内并发交替地完成,随着研究的进展,poll/epoll方法(select的改善)也相继出现,其功能和select类似,可以同时监视多个描述符的读写就绪状况,即在一个线程内同时处理多个socket的I/O请求。
在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常事件,帮助调用者寻找当前就绪的设备。
原理图如下所示,select首先进行系统调用,当数据准备好时返回socket接口,然后使用recv接受socket发送过来的数据,并进行处理。
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;
(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大;
(3)select支持的文件描述符数量太小了,默认是1024;
每次调用Select都需要将进程加入到所有监视Socket的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个FDS列表传递给内核,有一定的开销。所以为了减少遍历次数,并保存就绪的socket(fd),研究出现了epoll方法,它是select和poll的增强版本。
epoll伪代码如下,将所有socket加入到等待队列中,当有数据到达时,可以直接找到对应的socket,无需遍历;
使用快递来进行类比,select:需要对快递编号依次遍历,看看有没有快递员到达,也就是当快递到达时没法直接知道快递编号,需要依次遍历进行获取;epoll:当有快递到达时,可以直接得知快递编号。
server.c
//server.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
int server_sockfd, client_sockfd;
int server_len, client_len;
struct sockaddr_in server_address;
struct sockaddr_in client_address;
int ret;
fd_set testfds,readfds;
//1.建立服务器端socket,用于描述IP地址和端口,通过socket向网络发请求或应答网络请求
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立服务器端socket
//2.绑定侦听的IP地址和端口
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY=0,表示侦听全部IP地址
server_address.sin_port = htons(8888); //端口
server_len = sizeof(server_address);
bind(server_sockfd, (struct sockaddr *)&server_address, server_len); //将套接字套接字绑定到地址server上。
//3.侦听
listen(server_sockfd, 7); //监听队列最多容纳7个
FD_ZERO(&readfds);
FD_SET(server_sockfd, &readfds);//可读集合初始化为[server_sockfd,]
while(1)
{
char ch;
int nread;
printf("server waiting......\n");
/*无限期阻塞,并测试文件描述符变动 */
testfds = readfds; //将需要监视的描述符集copy到select查询队列中,select会对其修改,所以一定要分开使用变量
ret = select(FD_SETSIZE, &testfds, (fd_set *)0,(fd_set *)0, (struct timeval *) 0); //数据准备就绪,则文件描述符会发生变化
/* select:寻找当前准备就绪的设备,当可读、可写、异常任一文件描述符准备就绪,则select返回,每次结束后,select重新赋值
* 当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位(变成ready),使得进程可以获得这些文件描述符从而进行后续的读写操作
* readfds:监视的socket,testfds:监视的socket中数据就绪的socket
* select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
* maxfdp(FD_SETSIZE):被监听文件描述符总数;readfds、writefds、exceptset:分别指向可读、可写和异常事件
* timeout: 等于NULL(0)表示无限等待
*/
//testfds :初始化的可读集合里面哪一个准备就绪了,返回的testfds就等于几,FD_SETSIZE等于初始化可读集合总数+1
//testfds初始是监听的集合,返回值是准备好的集合
if(ret < 1) //ret:0表示超时,-1表示失败
{
perror("select error");
exit(1);
}
/*扫描所有的文件描述符*/
for(int fd = 0; fd < FD_SETSIZE; fd++) /*循环遍历*/
{
if(FD_ISSET(fd,&testfds)) //有数据准备就绪,fd为进来的请求,定位置位
{
// printf("fd:%d\n",fd);
if(fd == server_sockfd) /*判断是否为服务器套接字(3),是则表示为客户请求连接。*/
{
client_len = sizeof(client_address);
client_sockfd = accept(server_sockfd,(struct sockaddr *)&client_address, &client_len); //接收客户端socket
FD_SET(client_sockfd, &readfds);//将客户端socket加入到集合中(可读集合扩充了)
printf("adding client on fd %d......\n", client_sockfd);
}
else /*客户端socket中有数据请求时*/
{
char cmd[256];
ret = recv(fd, cmd, sizeof(cmd), 0); //接受数据(命令)
// printf("cmd:%s",cmd);
if(ret < 1 || strncmp(cmd, "exit", 4) ==0)/*客户数据请求完毕,关闭套接字,从集合中清除相应描述符 */
{
close(fd);
FD_CLR(fd, &readfds); //去掉关闭的fd
printf("client on fd %d closed......\n", fd);
}
else /*对数据进行处理*/
{
/*处理客户数据请求*/
if(strncmp(cmd, "put", 3) ==0) //上传文件
{
char filename[256] = "";
char filepath[256] = "./data/"; //上传的文件存放在服务器的位置
for(int i=4;cmd[i]!='\0';i++)
{
filename[i-4]=cmd[i];
}
strcat(filepath,filename);
// printf("recv cmd on fd %d: %s\n", fd,cmd);
char a[2] = "ok";
ret = send(fd, a,sizeof(a), 0); //通知客户端收到,防止粘包
char buffer[1024];
FILE * fp = fopen(filepath,"w"); //保存客户端发送的文件内容
bzero(buffer,1024);
int length = 0;
if((length = recv(fd,buffer,1024,0))>0) //接收并写到文件
{
if(length < 0)
{
printf("Recieve Data Failed!\n");
break;
}
int write_length = fwrite(buffer,sizeof(char),length,fp);
if (write_length0)
{
if(send(fd,buffer,file_length,0)<0) //向客户端发送文件内容
{
printf("Send File:\t%s Failed\n", filepath);
break;
}
bzero(buffer, 1024);
printf("put all file(file_length = %d) to fd %d...\n",file_length,fd);
}
fclose(fp);
}
else
{
char a[2] = "on";
ret = send(fd, a,sizeof(a), 0); //通知客户端文件不存在
printf("get file is not valid\n");
}
}
}
}
}
}
}
return 0;
}
client.c
//client.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
int client_sockfd,len,ret;
struct sockaddr_in address;//服务器端网络地址结构体
char cmd[256];
//1.建立客户端socket
client_sockfd = socket(AF_INET, SOCK_STREAM, 0);
//2.连接服务端
address.sin_family = AF_INET;
address.sin_addr.s_addr = inet_addr("127.0.0.1");
address.sin_port = htons(8888);
len = sizeof(address);
ret = connect(client_sockfd, (struct sockaddr *)&address, len);
if(ret == -1)
{
perror("connect error");
exit(1);
}
printf("------Welcome!!-----\n");
printf(" (helps:information you need)\n");
while(1)
{
printf(">>>");
bzero(cmd,256);
if(fgets(cmd,256,stdin) == NULL)
{
printf("input Error!\n");
return -1;
}
cmd[strlen(cmd)-1]='\0'; //fgets函数读取的最后一个字符为换行符,此处将其替换为'\0'
if(strncmp(cmd, "put", 3) ==0 ) //上传文件(空格是为了标准化格式)
{
if(cmd[3]=='\0'||cmd[4]=='\0') //直接回车
{
printf("No filename follows after put cmd!\n");
continue;
}
char filename[256] = "";
char filepath[256] = "./data/"; //上传文件在本地的位置
for(int i=4;cmd[i]!='\0';i++)
{
filename[i-4]=cmd[i];
}
strcat(filepath,filename);
if((access(filepath,F_OK))!=-1) //文件存在
{
ret = send(client_sockfd, cmd,sizeof(cmd), 0); //发送命令
char a[2];
ret = recv(client_sockfd, a, sizeof(a), 0); //防止粘包
if(strncmp(a, "ok" , 2) == 0) //收到服务器的回应
{
char buffer[1024];
FILE * fp = fopen(filepath,"r");
bzero(buffer, 1024);
int file_length = 0;
while( (file_length = fread(buffer,sizeof(char),1024,fp))>0)
{
//上传文件内容
if(send(client_sockfd,buffer,file_length,0)<0)
{
printf("Send File:\t%s Failed\n", filepath);
break;
}
bzero(buffer, 1024);
printf("put all file(file_length = %d)...\n",file_length);
} //这段代码是循环读取文件的一段数据,在循环调用send,发送到客户端,这里强调一点的TCP每次接受最多是1024字节,多了就会分片,因此每次发送时尽量不要超过1024字节。
fclose(fp);
}
}
else{
printf("File is not valid\n");
continue;
}
}
else if(strncmp(cmd, "get" , 3) == 0) //下载文件
{
if(cmd[3]=='\0'||cmd[4]=='\0') //直接回车的情况
{
printf("No filename follows after get cmd!\n");
continue;
}
char filename[256] = "";
char filepath[256] = "./data/"; //下载的文件在服务器的位置
for(int i=4;cmd[i]!='\0';i++)
{
filename[i-4]=cmd[i];
}
strcat(filepath,filename);
ret = send(client_sockfd,cmd,sizeof(cmd), 0); //发送命令
char a[2];
ret = recv(client_sockfd, a, sizeof(a), 0); //防止粘包
if(strncmp(a, "ok" , 2) == 0) //收到服务器的回应,文件存在
{
char buffer[1024];
FILE * fp = fopen(filepath,"w"); //保存服务器发送的文件内容
bzero(buffer,1024);
int length = 0;
if((length = recv(client_sockfd,buffer,1024,0))>0) //接收并写到文件
{
if(length < 0)
{
printf("Recieve Data Failed!\n");
break;
}
int write_length = fwrite(buffer,sizeof(char),length,fp);
if (write_length
操作说明:
#原始文件#
--server
--/data
--5.txt 6.txt 服务器中的文件
--client
--/data
--1.txt 2.txt 客户端中的文件1.打开服务器
cd server
gcc server.c -o server
./server2.打开客户端1
cd client
gcc client.c -o client
./client#向服务器上传文件1.txt,成功后会出现/server/data/1.txt
put 1.txt#从服务器中下载文件5.txt,成功后会出现/client/data/5.txt
get 5.txt3.打开客户端2
cd client
gcc client.c -o client
./client#向服务器上传文件2.txt,成功后会出现/server/data/2.txt
put 2.txt#从服务器中下载文件6.txt,成功后会出现/client/data/6.txt
get 6.txt
server.c
//server.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
int server_sockfd, client_sockfd;
int server_len, client_len;
struct sockaddr_in server_address;
struct sockaddr_in client_address;
int ret;
//1.建立服务器端socket,用于描述IP地址和端口,通过socket向网络发请求或应答网络请求
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立服务器端socket
//2.绑定侦听的IP地址和端口
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY=0,表示侦听全部IP地址
server_address.sin_port = htons(8787); //端口
server_len = sizeof(server_address);
bind(server_sockfd, (struct sockaddr *)&server_address, server_len); //将套接字套接字绑定到地址server上。
//3.侦听
listen(server_sockfd, 7); //监听队列最多容纳7个
int epollfd = epoll_create(10); //创建文件描述符(10表示epollfd上能关注的最大fd数)
struct epoll_event tmp,epevents[5]; //创建epoll对象
tmp.events = EPOLLIN; //表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
tmp.data.fd = server_sockfd;
epoll_ctl(epollfd,EPOLL_CTL_ADD,server_sockfd,&tmp);//将需要监视的socket加入到epollfd中(关注了server_sockfd),等待队列
/*(epoll_create()返回值,动作,需要监听的socket,需要监听的内容(可读))*/
while(1)
{
char ch;
int nread;
printf("server waiting......\n");
//获取准备好的描述符事件
int num = epoll_wait(epollfd,epevents,5,-1); //等待epevents对象,可读时通知,读到后就改为写时通知,epollfd为需要监视的socket集合
/*select扫描所有的fd,然后准备就绪的fd放在等待队列进行数据处理
*epoll 不用,其等待队列已经存放于events中(即epevents[i].data.fd),只需要判断它的fd是不是可读
*select每次都需要添加可读等待队列(fd),然后阻塞,遍历所有的fd-size。
*但是epoll不用,其等待队列(fd)已经存放于events中,一直阻塞,只需要遍历epevents事件判断其fd是否可读
*/
/*扫描所有的文件描述符*/
for(int i = 0; i < num; i++) /*num为等待到的连接数(1)*/
{
if(!(epevents[i].events & EPOLLIN)) //如果不是读事件,直接跳过 [&:两个都运行,有一个为假就不成立]
{
continue;
}
if(epevents[i].data.fd == server_sockfd) /*判断是否为服务器套接字(3),是则表示为客户请求连接。*/
{
client_len = sizeof(client_address);
client_sockfd = accept(server_sockfd,(struct sockaddr *)&client_address, &client_len); //接收客户端socket
tmp.events = EPOLLIN;
tmp.data.fd = client_sockfd;
ret = epoll_ctl(epollfd,EPOLL_CTL_ADD,client_sockfd, &tmp); //将客户端socket加入到epollfd中
printf("adding client on fd %d......\n", client_sockfd);
}
else /*客户端socket中有数据请求时*/
{
char cmd[256];
ret = recv(epevents[i].data.fd, cmd, sizeof(cmd), 0); //接受数据(命令)
if(ret < 1 || strncmp(cmd, "exit", 4) ==0)/*客户数据请求完毕,关闭套接字,从集合中清除相应描述符 */
{
close(epevents[i].data.fd);
epoll_ctl(epollfd,EPOLL_CTL_DEL,epevents[i].data.fd,NULL);//删除已经注册的fd
printf("client on fd %d closed......\n", epevents[i].data.fd);
}
else /*对数据进行处理*/
{
/*处理客户数据请求*/
if(strncmp(cmd, "put", 3) ==0) //上传文件
{
char filename[256] = "";
char filepath[256] = "./data/"; //上传的文件存放在服务器的位置
for(int i=4;cmd[i]!='\0';i++)
{
filename[i-4]=cmd[i];
}
strcat(filepath,filename);
// printf("recv cmd on fd %d: %s\n", fd,cmd);
char a[2] = "ok";
ret = send(epevents[i].data.fd, a,sizeof(a), 0); //通知客户端收到,防止粘包
char buffer[1024];
FILE * fp = fopen(filepath,"w"); //保存客户端发送的文件内容
bzero(buffer,1024);
int length = 0;
if((length = recv(epevents[i].data.fd,buffer,1024,0))>0) //接收并写到文件
{
if(length < 0)
{
printf("Recieve Data Failed!\n");
break;
}
int write_length = fwrite(buffer,sizeof(char),length,fp);
if (write_length0)
{
if(send(epevents[i].data.fd,buffer,file_length,0)<0) //向客户端发送文件内容
{
printf("Send File:\t%s Failed\n", filepath);
break;
}
bzero(buffer, 1024);
printf("put all file(file_length = %d) to fd %d...\n",file_length,epevents[i].data.fd);
}
fclose(fp);
}
else
{
char a[2] = "on";
ret = send(epevents[i].data.fd, a,sizeof(a), 0); //通知客户端文件不存在
printf("get file is not valid\n");
}
}
}
}
}
}
close(epollfd);
return 0;
}
client.c
//client.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
int client_sockfd,len,ret;
struct sockaddr_in address;//服务器端网络地址结构体
char cmd[256];
//1.建立客户端socket
client_sockfd = socket(AF_INET, SOCK_STREAM, 0);
//2.连接服务端
address.sin_family = AF_INET;
address.sin_addr.s_addr = inet_addr("127.0.0.1");
address.sin_port = htons(8787);
len = sizeof(address);
ret = connect(client_sockfd, (struct sockaddr *)&address, len);
if(ret == -1)
{
perror("connect error");
exit(1);
}
printf("------Welcome!!-----\n");
printf(" (helps:information you need)\n");
while(1)
{
printf(">>>");
bzero(cmd,256);
if(fgets(cmd,256,stdin) == NULL)
{
printf("input Error!\n");
return -1;
}
cmd[strlen(cmd)-1]='\0'; //fgets函数读取的最后一个字符为换行符,此处将其替换为'\0'
if(strncmp(cmd, "put", 3) ==0 ) //上传文件(空格是为了标准化格式)
{
if(cmd[3]=='\0'||cmd[4]=='\0') //直接回车
{
printf("No filename follows after put cmd!\n");
continue;
}
char filename[256] = "";
char filepath[256] = "./data/"; //上传文件在本地的位置
for(int i=4;cmd[i]!='\0';i++)
{
filename[i-4]=cmd[i];
}
strcat(filepath,filename);
if((access(filepath,F_OK))!=-1) //文件存在
{
ret = send(client_sockfd, cmd,sizeof(cmd), 0); //发送命令
char a[2];
ret = recv(client_sockfd, a, sizeof(a), 0); //防止粘包
if(strncmp(a, "ok" , 2) == 0) //收到服务器的回应
{
char buffer[1024];
FILE * fp = fopen(filepath,"r");
bzero(buffer, 1024);
int file_length = 0;
while( (file_length = fread(buffer,sizeof(char),1024,fp))>0)
{
//上传文件内容
if(send(client_sockfd,buffer,file_length,0)<0)
{
printf("Send File:\t%s Failed\n", filepath);
break;
}
bzero(buffer, 1024);
printf("put all file(file_length = %d)...\n",file_length);
} //这段代码是循环读取文件的一段数据,在循环调用send,发送到客户端,这里强调一点的TCP每次接受最多是1024字节,多了就会分片,因此每次发送时尽量不要超过1024字节。
fclose(fp);
}
}
else{
printf("File is not valid\n");
continue;
}
}
else if(strncmp(cmd, "get" , 3) == 0) //下载文件
{
if(cmd[3]=='\0'||cmd[4]=='\0') //直接回车的情况
{
printf("No filename follows after get cmd!\n");
continue;
}
char filename[256] = "";
char filepath[256] = "./data/"; //下载的文件在服务器的位置
for(int i=4;cmd[i]!='\0';i++)
{
filename[i-4]=cmd[i];
}
strcat(filepath,filename);
ret = send(client_sockfd,cmd,sizeof(cmd), 0); //发送命令
char a[2];
ret = recv(client_sockfd, a, sizeof(a), 0); //防止粘包
if(strncmp(a, "ok" , 2) == 0) //收到服务器的回应,文件存在
{
char buffer[1024];
FILE * fp = fopen(filepath,"w"); //保存服务器发送的文件内容
bzero(buffer,1024);
int length = 0;
if((length = recv(client_sockfd,buffer,1024,0))>0) //接收并写到文件
{
if(length < 0)
{
printf("Recieve Data Failed!\n");
break;
}
int write_length = fwrite(buffer,sizeof(char),length,fp);
if (write_length
操作说明:
#原始文件#
--server
--/data
--5.txt 6.txt 服务器中的文件
--client
--/data
--1.txt 2.txt 客户端中的文件1.打开服务器
cd server
gcc server.c -o server
./server2.打开客户端1
cd client
gcc client.c -o client
./client#向服务器上传文件1.txt,成功后会出现/server/data/1.txt
put 1.txt#从服务器中下载文件5.txt,成功后会出现/client/data/5.txt
get 5.txt3.打开客户端2
cd client
gcc client.c -o client
./client#向服务器上传文件2.txt,成功后会出现/server/data/2.txt
put 2.txt#从服务器中下载文件6.txt,成功后会出现/client/data/6.txt
get 6.txt
参考博客:
https://www.cnblogs.com/skyfsm/p/7079458.html
https://www.cnblogs.com/-zyj/p/5719923.html