文章目录
前言
前面了解的服务器框架其实实质都是阻塞 I/O 类型的,从开始的迭代型,到后来为了提高性能,提高并发性,因此改成了多进程,但是进程的开销还是很大的,因此我们有设计出了多线程的方式。但是随着性能的提升,传统的 accept 方式其阻塞性质开始成为性能瓶颈,因此我们使用了多个线程同时 accept 的方式来提高并发处理的能力。
但是其本质没有偏离阻塞 I/O 的基本特性,其原因就是我们的 API 接口均是阻塞型的,没有请求就只有死等,没有连接就耗着。
因此提出了 select 模型。
阻塞和非阻塞两种 I/O 模式
- 阻塞模式:执行 I/O 操作完成前会一直等待,不会将控制权交给程序。套接字传统(connect、accept、recv 或 recvfrom 这样的阻塞接口)默认为阻塞模式。可以通过多进程或者多线程技术进行优化处理。
阻塞模式下,进程或者是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或者是线程就被阻塞(死等在被阻塞的地方),函数不会立即返回。
- 非阻塞模式:执行 I/O 操作,函数会立即返回并交出控制权。这种模式使用起来比较复杂,因为函数在没有运行完成就进行返回,会不断地返回错误(当非阻塞 socket 的 TCP连接正在进行时,Linux的错误号
EINPROGRESS
,Windows的错误号为WSAEWOULDBLOCK
)。但功能强大。它能够监视我们需要监视的文件描述符的变化情况 —— 读写或异常。
非阻塞non-block模式下,进程或者线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件每发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以非阻塞模式效率较高。
为了解决这个问题,提出了进行 I/O 操作的一些 I/O 模型。
Linux下最常见的三种就是 select
模式,poll
模式,epoll
模型,当然也还有一些异步的或者事件驱动型的服务器模型。
同样 Windows 下的 Winsock 也提供了一些 I/O 模型,常用的有五种:select
, WSAAsyncSelect
, WSAEventSelect
, Overlapped
, Completion
等。
select 系统调用
select起源及描述
- select 最早于1983年出现在4.2BSD中,select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。
- select 系统调用是用来让我们的程序监视多个文件句柄(file descriptor)的状态变化的。通过
select()
系统调用来监视多个文件描述符的数组,当select()
返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。select
系统调用是用来让我们的程序监视多个文件描述符的状态变化的。程序会停在 select 这里等待,直到被监视的文件描述符有一个或多个发生了状态改变。select()
的机制中提供fd_set
的数据结构,实际上就是 long 类型的数组,每一个数组元素都能与一个打开的文件句柄建立联系,建立联系的工作由程序员完成,当调用 select() 时,由内核根据 IO 状态修改 fd_set 的内容,由此来通知执行了 select() 的进程哪些 socket 或 文件可读可写。
【参数详解】
ndfs
:select 监视的文件句柄数,视进程中打开额文件数而定,一般设为监视各文件中的最大文件描述符加1readfds
:这个文件描述符集合监视文件集中的任何文件是否有数据可读,当 select 函数返回额时候,readfds 将清除其中不可读的文件描述符,只留下可读的文件描述符。writefds
:这个文件描述符集合监视文件集中的任何文件是否有数据可写,当 select 函数返回额时候,writefds 将清除其中不可写的文件描述符,只留下可写的文件描述符。exceptfds
:这个文件集合监视文件集中的任何文件是否发生错误,其实,它可用于其它的用途。例如,监视带外数据OOB
,带外数据使用MSG_OOB
标志发送到套接字上。当 select 函数返回的时候,expectfds 将清除其中的其它文件描述符,只留下标记有OOB
数据的文件描述符。timeout
:本次的 select() 的超时结束时间。这个参数也至关重要,它可以使 select 处于三种状态:
- 若将 NULL 以形参传入,即不传入时间结构,就是将 select 置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符状态发生改变为止。
- 若将时间值设为 0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值。
- timeout 的值大于0,这就是等待的超出时间,即 select 在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
函数的返回值
返回值 | 描述 |
---|---|
正值 | 表示监视的文件集合中由文件描述符符合要求 |
零值 | 表示 select 监视超时 |
负值 | 表示发生了错误,错误值由 errno 指定 |
宏操作
返回值 | 描述 |
---|---|
FD_ZERO(fd_set *set) | 清除描述词组 set 的全部位 |
FD_SET(int fd, fd_set *set) | 用来设置描述词组 set 中相关 fd 的位 |
FD_ISSET(int fd, fd_set *set) | 测试描述词组 set 中相关 fd 的位是否为真 |
FD_CLR(int fd, fd_set *set) | 清除词组 set 中相关 fd 位 |
注意事项
- 对于可写性的检查,最好放在需要写数据的时候进行检查。如果和可读性放在同一个地方进行检查,那么 select 很可能每次都是因为写检查成功而返回。
select()
调用会清空传递给它的集合参数中的内容,也就是会清空readfds
、writefds
、expectfds
这三个指针参数所指定的描述符集合。因此,在每次调用select()
之前必须重新初始化并把需要监视的描述符填写到相应的描述符集合中。select()
调用也会清空timeout
指针所指向的struct timeval
结构,所以在每次调用select()
之前也要重新填充timeout
指针所指向的struct timeval
结构。
select 使用流程
传统的单进程服务器一旦accept
了客户端的 TCP 连接后,就转到客户请求的处理,处理完成后才能再一次调用accept
来接受下一个客户端的 TCP 连接和请求。
为了更加提高单进程服务器的性能,使用select
这种 IO 复用的模式,同时监听已经连接的 socket
端口和正在监听的服务器listening
端口,这样一来,就可以大大提升服务器处理并发请求的能力。
select 的使用方法如下:
1. 定义 fd_set
select 允许我们监听来自标准输入,标准输出,标准错误输出的 IO 信号,监听标准输入 IO 信号集。
2. 注册将要被监听的 fd
FD_SET(int fd, fd_set *set);
通过 FD_SET 和 FD_CLR 可以注册和清除某个 fd_set 内的 fd 项,使得在调用 select 的时候可以监听或者取消监听某个 fd。
3. 如果 IO 信号到达,识别并处理
通过FD_ISSET
可以判断 select 所监听的 fd_set 上的 IO 是否有状态变化,一旦返回 true,则可以对该 fd 进行操作。
4. select 使用事项及技巧
- 使用 select 时应注意,如果 select 有 timeout 设置,那么每次在 select 之前都需要重新设置一下 timeout 的值,因为每次 select 返回会修改 timeout 的值。
- 本例中,如果我们在某次 select 中捕获到 listenfd 的 IO 状态有变化,也就是说有新的客户端连接,我们不会马上做客户端的请求处理,而是把连接到的 socketfd 插入到 select 的监听集合中,然后继续探测其它监听集是否有 IO 状态变化(这里的其它监听集就是每个已经连接的客户端的 socketfd 的状态),如果有变化则马上处理客户端的请求。这样做的好处是我们及时处理了已连接的客户端的请求,而不是被新连接的客户端的请求所抢占,减少了旧客户端被饿死的情况发生。
我们的程序在单进程服务器上使用select,所以适合简单客户请求处理,也就是短连接的情况,如果长时间服务于多个客户,可以使用 fork 多进程或者 pthread 多线程加以辅助。
Linux Socket 编程中 select 的常见用处
1. accept 函数的非阻塞实现
当服务器程序调用了 listen 函数后,此时就可以接受客户端的 connect 请求,3次握手后,服务器八简历的连接存入队列中,等待 accept 函数的调用。每调用一个 accept,程序就从队列中取出一个已建立连接的 socket。默认方式下,accept 处于阻塞状态,将套接字文件属性设置为非阻塞时,accept 处于非阻塞状态(其实与该套接字相关的系统调用都是非阻塞的了)。
如果将正在 listen 的 socket 设置到 readfds 中,调用 select,如果有客户端 connect,select 将会返回正值,通过宏 FD_ISSET 可检测到该 socket 可读,此时再用 accept 接受新的 socket,进行读写。
因此当
select
返回正值时,即检测到有数据请求时,
我们可以使用FD_ISSET(listenfd, &readset)
检测服务器的监听套接字是否有数据,如果有说明监听到了客户端的连接请求,那么我们调用 accept 来获取客户端的连接。
由此我们的accept
函数的非阻塞实现就是非阻塞的,因为我们只有当监听到了服务器监听套接字有连接请求时,才会 accept。
处理完毕后再循环判断其它客户端连接套接字 connfd 有没有数据请求。
2. connect 函数的非阻塞实现(TCP)
- 将打开的 socket 设置为非阻塞。
- 发出 connect 调用,这是返回 -1,但是 errno 被设置为 EINPROGRESS,意即 connect 仍旧在进行还没有完成。
- 将打开的 socket 放进被监视的可写(注意不是可读)文件集合中,用 select 进行监视,如果可写,用 getsockopt 函数来得到 error 的值,如果为零则 connect 成功。
关于非阻塞connect的详细知识可参考:非阻塞Connect对于select时应注意问题
3. select 检测对方 socket 连接关闭
使用 select
监视是否有数据可读,当监视到可读返回正值后,使用 recv
函数对套接字进行读操作,recv 函数返回正值表示正常的读操作,若返回 0 或者 -1 表示对方连接已经关闭。
用阻塞式 socket 发送和接收数据,但是由于没有建立心跳机制,远端服务器在一定时间内(系统设定)没有活动数据,就会断开连接。无奈只能每次在发送数据之前先检测多方是否已经断开连接,当然在发送数据之前检测的目的是保证这次发数据不会因为对方断开连接而接收数据失败(对方断开连接,发送还是会成功的)。这个方法比较挫,使用了 select + recv
fd_set read_set;
struct timeval t_o;
FD_ZERO(read_set);
FD_SET(sockFd, &read_set);
// 设置超时秒数
t_o.tv_sec = n;
ret = select(sockFd + 1, &read_set, NULL, NULL, &t_o);
if(ret == 1)
{
if(FD_ISSET(sockFd, &read_set))
{
count = recv(sockfd, buf, LEN, 0);
if(count <= 0)
{
// 这种情况可以认为是链路关闭
}
}
}
另外关于主动写 socket 时对方突然关闭连接的处理,则可以简单地捕捉到信号
SIGPIPE
并作出相应关闭本地 socket 等等的处理。
SIGPIPE
的解释是:写入无读者方的管道
此外我们也可以使用 TCP连接探测中的 Keepalive 和心跳包的机制来保活或者判断客户端的断开。
select 模式的单进程非阻塞 accept 服务器
- SocketAPI.h
- 服务器(server.cc,server.h)
- 客户端(client.cc,client.h)
1. SocketAPI.h
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <string>
#include <cstring>
#include <fcntl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <unistd.h>
#include <assert.h>
#include <signal.h>
#include <errno.h>
#include <sys/select.h>
using namespace std;
#define LISTEN_QUEUE SOMAXCONN
#define MAX_FILENAME_SIZE 256
#define BUFFER_SIZE 4096
#define IP_SIZE 20
class SockAPI{
public:
//创建套接字
static int Socket(int type)
{
int sockfd;
sockfd = socket(AF_INET, type, 0);
if(sockfd < 0)
{
assert(sockfd >= 0);
perror("socket error");
exit(1);
}
cout << "socket success\n";
return sockfd;
}
//命名套接字
static void Bind(int sockfd, int port)
{
int ret;
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
//server_addr.sin_addr.s_addr = inet_addr("192.....");
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(port);
ret = bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if(ret < 0)
{
assert(ret >= 0);
perror("bind error");
exit(1);
}
cout << "bind success\n";
}
//监听端口
static void Listen(int sockfd)
{
int ret = listen(sockfd, LISTEN_QUEUE);
if(ret < 0)
{
assert(ret = 0);
perror("listen error");
exit(1);
}
cout << "listening\n";
}
//发起连接请求
static void Connect(int sockfd, char* ip, int port)
{
int ret;
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(ip);
server_addr.sin_port = htons(port);
ret = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if(ret < 0)
{
assert(ret = 0);
perror("connect error");
exit(1);
}
cout << "connect success\n";
}
//接受请求
static int Accept(int sockfd, string &out_ip, int &out_port)
{
int connfd;
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
connfd = accept(sockfd, (struct sockaddr*)&client_addr, &len);
if(connfd < 0)
{
assert(connfd = 0);
perror("accept error");
exit(1);
}
out_ip = inet_ntoa(client_addr.sin_addr);
out_port = htons(client_addr.sin_port);
cout << "accept success " << out_ip << endl; ;
return connfd;
}
//关闭连接
static void Close(int connfd)
{
close(connfd);
}
};
2. server.h
#include "SocketAPI.h"
class Server{
public:
/* 服务器接收从客户端传送来的文件 */
static void TcpServerPullFile(
int connfd, /* 服务器与客户端通讯的套接字文件 */
string ip, /* 与之通信的客户端的信息 */
char *fileServerRoot) /* 上传文件的存储路径 */
{
char buffer[BUFFER_SIZE];
char filename[MAX_FILENAME_SIZE];
char fileServerPath[MAX_FILENAME_SIZE]/* = fileServerRoot*/;
// 定义文件流
FILE *stream;
int count; /* 发送文件名的字节数目 */
int dataLength; /* 接收到的数据大小 */
int writeLength; /* 实际写入的数据大小 */
int flag = 0;
bzero(buffer, BUFFER_SIZE);
/*
* 向客户端提示输入文件路径提示...
*
* strcpy(buffer, "请输入要传输的文件的完整路径:");
strcat(buffer, "\n");
send(new_server_socket, buffer, BUFFER_SIZE, 0);
bzero(buffer, BUFFER_SIZE);
*/
/* 首先获取客户端发送过来的文件名 */
count = recv(connfd, buffer, BUFFER_SIZE, 0);
if(count < 0)
{
perror("获取文件名失败...\n");
exit(1);
}
else
{
strncpy(filename, buffer, strlen(buffer) > MAX_FILENAME_SIZE ? MAX_FILENAME_SIZE : strlen(buffer));
strcpy(fileServerPath, fileServerRoot);
strcat(fileServerPath, filename);
printf("\n获取客户端发送过来的文件名成功...\n");
printf("文件名[%s]\n", filename);
printf("文件存储路径[%s]\n\n", fileServerPath);
}
// 服务器接受数据, 首先打开一个文件流
if((stream = fopen(fileServerPath, "w")) == NULL)
{
perror("file open error...\n");
exit(1);
}
else
{
bzero(buffer,BUFFER_SIZE);
}
printf("正在接收来自%s的文件....\n",ip.c_str());
dataLength = 0;
/* 先将数据接受到缓冲区buffer中,再写入到新建的文件中 */
while((dataLength = recv(connfd, buffer, BUFFER_SIZE, 0)) > 0)
{
flag++;
if(flag == 1)
{
printf("正在接收来自%s的文件....\n", ip.c_str());
}
if(dataLength < 0)
{
printf("接收错误i\n");
exit(1);
}
/* 向文件中写入数据 */
writeLength = fwrite(buffer, sizeof(char), dataLength, stream);
if(writeLength != dataLength)
{
printf("file write failed\n");
exit(1);
}
bzero(buffer,BUFFER_SIZE);
}
if(flag > 0)
{
printf("%s的文件传送完毕\n", ip.c_str());
}
if(flag==0)
{
printf("%s的文件传输失败\n", ip.c_str());
}
fclose(stream);
}
/* 服务器将文件发送到客户端
*
* 当用户选择了下载文件后,服务器将执行此操作
*
* */
static void TcpServerPushFile(
int connfd, /* 服务器与客户端通讯的套接字文件 */
struct sockaddr_in clientAddr, /* 与之通信的客户端的信息 */
char *filePath) /* 带发送至客户端的文件路径 */
{
//send file imformation
char buff[BUFFER_SIZE];
char filename[MAX_FILENAME_SIZE];
int count;
FILE *stream;
/* 先将文件名发送给客户端
* 发送文件名时只需要发送filePath最后的文件名filename就可以了
* */
bzero(buff, BUFFER_SIZE);
strcpy(filename, strrchr(filePath, '/') + 1);
strncpy(buff, filename, strlen(filename) > MAX_FILENAME_SIZE ? MAX_FILENAME_SIZE : strlen(filename));
count = send(connfd, buff, BUFFER_SIZE, 0);
printf("服务器待发送的文件名[%s]..\n", filename);
if(count < 0)
{
perror("Send file information");
exit(1);
}
/* 服务器开始读取并且发送文件 : */
if((stream = fopen(filePath, "rb")) == NULL)
{
printf("File :%s not found!\n",filePath);
}
printf("服务器打开文件成功...\n");
printf("正在向客户端发送文件...\n");
bzero(buff, BUFFER_SIZE);
int fileBlockLength = 0;
while((fileBlockLength = fread(buff, sizeof(char), BUFFER_SIZE, stream)) > 0)
{
printf("读取了:%d个数据...\n",fileBlockLength);
if(send(connfd, buff, fileBlockLength, 0) < 0)
{
perror("Send file error...\n");
perror("向客户端发送文件失败...\n");
exit(1);
}
bzero(buff,BUFFER_SIZE);
}
fclose(stream);
printf("服务器发送文件成功\n");
}
// 处理客户端的请求
static void RaiseClientRequest(int connFd, string ip)
{
int count = -1;
char buffer[BUFFER_SIZE];
// 首先测试接收客户端发来的数据
bzero(buffer, BUFFER_SIZE);
count = recv(connFd, buffer, BUFFER_SIZE, 0);
if(count < 0) // 这两种情况都可认为是链路关闭
{
printf("接收来自 %s 的数据错误, 错误码errno = %d....\n", ip, errno);
SockAPI::Close(connFd);
}
else
printf("接收%d个数据 : %s\n", count, buffer);
// 向客户端反馈数据
bzero(buffer, BUFFER_SIZE);
strcpy(buffer, "I am received data");
if((count = send(connFd, buffer, strlen(buffer) + 1, 0)) < 0)
printf("发送数据[%s] 失败[错误码 = %d]...\n", buffer, errno);
else
printf("发送数据[%s]成功...\n", buffer);
printf("\n");
}
};
3. server.cc
/**********************************************************
> File Name: server.c
> Author: Darwlr
> Created Time: 2020年08月17日 星期一 12时22分10秒
*********************************************************/
#include "server.h"
#define TCP_SERVER_PORT 6666 /* 服务器的端口 */
void SignalChild(int signo)
{
pid_t pid;
int status;
while((pid = waitpid(-1, &status, WNOHANG) > 0))
{
printf("child %d terminated\n", pid);
}
return;
}
extern int errno;
int main(int argc, char *argv[])
{
/**********************************************************
*
* 创建并初始化服务器套接字
*
**********************************************************/
int listenFd;
/* 创建套接字 */
listenFd = SockAPI::Socket(SOCK_STREAM);
/* 绑定端口 */
SockAPI::Bind(listenFd, TCP_SERVER_PORT);
/* 开始监听绑定的端口 */
SockAPI::Listen(listenFd);
// Value for select
int fd, maxfd, nready;
fd_set rset, allset;
struct timeval timeout;
// 初始化
maxfd = listenFd;
// allset 用来保存清除完标志的 fd_ret 信息,在每次处理完成后,赋值给 rset
FD_ZERO(&allset);
FD_SET(listenFd, &allset);
int connFd;
string out_ip;
int out_port;
while(1)
{
rset = allset; // rset和allset的搭配使得新加入的fd要等到下次select才会被监听
// 如果有 timeout 设置,那么在每次select之前都要重新设置一下timeout的值
timeout.tv_sec = 0;
timeout.tv_usec = 500000;
nready = select(maxfd + 1, &rset, (fd_set*)NULL, (fd_set*)NULL, &timeout);
if(nready < 0)
{
perror("select error...\n");
exit(-1);
}
else if(nready == 0) // 超时
{
printf(".");
fflush(stdout);
continue;
}
// 接收到请求后,检测数据请求的套接字来自哪些套接字
// 首先检测服务器监听套接字有没有数据
// 如果有的话说明监听到了客户端
// 应该调用 accept 来获取客户端的连接
// 接着检测客户端的连接套接字有没有数据连接
// 如果有的话,说明客户端跟服务器有通信需求
// 首先检测服务器监听套接字有没有数据
if(FD_ISSET(listenFd, &rset))
{
connFd = SockAPI::Accept(listenFd, out_ip, out_port);
// 新加入的描述符,还没判断是否可读写
// 将缓存的 allset 的对应 connFd置位,下次循环时即可监听 connFd
FD_SET(connFd, &allset);
if(connFd > maxfd)
maxfd = connFd;
//到此,监听套接字的信息已经被处理,应该先清除对应位
FD_CLR(listenFd, &rset);
if(--nready <= 0) // nready(用来辅助计数),这样就不(用遍历整个数组)
continue;
}
// 遍历套接字看哪些客户端连接套接字有数据请求
for(fd = 0; fd <= maxfd && nready > 0; fd++)
{
if(FD_ISSET(fd, &rset)) // 检测到客户端连接套接字 fd 有数据请求
{
// 单进程的环境下,不可以阻塞在这里,可以选择非阻塞
printf("\n开始与客户端通信,套接字描述符 fd = %d\n", fd);
Server::RaiseClientRequest(fd, out_ip);
// 清除 allset 的对应位,以备 fd 可以被 select 继续监听
FD_CLR(fd, &allset);
}
}
}
return EXIT_SUCCESS;
}
4. client.h
#include "SocketAPI.h"
class Client{
public:
/* 客户端将文件上传到服务器上 */
static void TcpClientPushFile(int socketFd, char *filePath)
{
FILE *stream;
char buffer[BUFFER_SIZE];
char filename[MAX_FILENAME_SIZE];
int count = 0;
bzero(buffer, BUFFER_SIZE);
strcpy(filename, strrchr(filePath, '/') + 1);
strncpy(buffer, filename, strlen(filename) > MAX_FILENAME_SIZE ? MAX_FILENAME_SIZE : strlen(filename));
count = send(socketFd, buffer, BUFFER_SIZE, 0);
printf("客户端待上传待文件名[%s]..\n", filename);
if(count < 0)
{
perror("Send file information");
exit(1);
}
/* 打开文件流 */
if((stream = fopen(filePath, "r")) == NULL)
{
printf("Can't open the file [%s]\n", filePath);
exit(1);
}
else
{
printf("客户端打开文件成功\n");
}
printf("正在向服务器传上传文件...\n");
count = 0;
/* 清空缓冲区 */
bzero(buffer, BUFFER_SIZE);
/* 不断读取并发送数据 */
while((count = fread(buffer, 1, BUFFER_SIZE, stream)) > 0)
{
// printf("count =%d\n", count);
if(send(socketFd, buffer, count, 0) < 0)
{
printf("send file error...\n");
break;
}
bzero(buffer, BUFFER_SIZE); /* 再次将缓冲区清空 */
}
printf("向服务器发送文件成功...\n");
/* 传送完毕后, 关闭文件流 */
if(fclose(stream))
{
printf("file close error\n");
exit(1);
}
else
{
printf("关闭文件流成功...\n");
}
}
/* 从服务器上下载文件 */
static void TcpClientPullFile(int socketFd, char *filePath)
{
char buff[BUFFER_SIZE];
char filename[MAX_FILENAME_SIZE];
int count, writeLength, dataLength;
FILE *stream;
bzero(buff,BUFFER_SIZE);
/* 首先获取服务器发送过来的文件名 */
count = recv(socketFd, buff, BUFFER_SIZE, 0);
if(count < 0)
{
perror("获取文件名失败...\n");
exit(1);
}
strncpy(filename, buff, strlen(buff) > MAX_FILENAME_SIZE ? MAX_FILENAME_SIZE : strlen(buff));
/* 开始接收文件 */
printf("Preparing download file : %s", filename);
/* 打开文件流 */
if((stream = fopen(filename, "wb+")) == NULL)
{
perror("create file %s error...\n");
perror("创建文件失败...\n");
exit(1);
}
bzero(buff, BUFFER_SIZE); /* 清空缓冲区 */
dataLength = 0;
while((dataLength = recv(socketFd, buff, BUFFER_SIZE, 0)) != 0)
{
if(dataLength < 0) /* 如果接收文件失败 */
{
perror("download error...\n");
perror("下载文件失败...\n");
exit(1);
}
/* 将接收到的文件数据写入文件中 */
writeLength = fwrite(buff, sizeof(char), dataLength, stream);
if(writeLength < dataLength) /* 如果写入的数据比实际接收到的数据少 */
{
perror("file write error...\n");
perror("写入文件失败...\n");
exit(1);
}
bzero(buff, BUFFER_SIZE); /* 清空缓冲区 */
}
printf("下载来自服务器%s的文件成功\n", filename);
printf("Receieved file:%s finished!\n", filename);
fclose(stream); /* 关闭文件流 */
}
// 向服务器发送请求
static void RaiseServerResponse(int socketFd)
{
char buffer[BUFFER_SIZE]; /* 数据缓冲区 */
int count; /* 接受或者发送的数据大小 */
bzero(buffer, BUFFER_SIZE);
strcpy(buffer, "How are you ?");
// 发送数据流
if((count = send(socketFd, buffer, strlen(buffer) + 1, 0)) < 0)
printf("send data[%s] error[errno = %d]...\n", buffer, errno);
else
printf("send data[%s] success...\n", buffer);
// 接收数据流
bzero(buffer, BUFFER_SIZE);
if((count = recv(socketFd, buffer, BUFFER_SIZE, 0)) < 0)
printf("recv data[%s] error[errno = %d]...\n", buffer, errno);
else
printf("recv data[%s] success...\n", buffer);
printf("\n");
}
};
5. client.cc
/**********************************************************
> File Name: server.c
> Author: Darwlr
> Created Time: 2020年08月17日 星期一 12时22分10秒
*********************************************************/
#include "client.h"
#define TCP_SERVER_PORT 6666
int main(int argc, char *argv[])
{
char serverIp[IP_SIZE]; /* 服务器的IP地址 */
if(argc >= 2) /* 参数过多的时候,提示用户 */
{
printf("You have given to much parameters...\n");
printf("Yous should give the IP address after %s\n without any other parametes...\n", (char *)argv[0]);
}
else if(argc == 1) /* 只有一个参数,则默认使用localhost(127.0.0.1) */
{
strcpy(serverIp, "127.0.0.1");
}
else
{
strcpy(serverIp, argv[1]);
}
/**********************************************************
*
* 创建并初始化套接字
*
**********************************************************/
int socketFd; /* 客户端的套接字信息 */
/* 开始创建套接字 */
/* SOCK_STREAM 面向连接的套接字,即TCP */
socketFd = SockAPI::Socket(SOCK_STREAM);
/* 尝试连接服务器 */
SockAPI::Connect(socketFd, serverIp, TCP_SERVER_PORT);
/**********************************************************
*
* 下面进行正常的套接字通信
*
**********************************************************/
Client::RaiseServerResponse(socketFd);
SockAPI::Close(socketFd);
return EXIT_SUCCESS;
}
【运行结果】
select 模式的多进程非阻塞 accept 服务器
server.cc
/**********************************************************
> File Name: server.c
> Author: Darwlr
> Created Time: 2020年08月17日 星期一 12时22分10秒
*********************************************************/
#include "server.h"
#define TCP_SERVER_PORT 6666 /* 服务器的端口 */
void SignalChild(int signo)
{
pid_t pid;
int status;
while((pid = waitpid(-1, &status, WNOHANG) > 0))
{
printf("child %d terminated\n", pid);
}
return;
}
extern int errno;
int main(int argc, char *argv[])
{
/**********************************************************
*
* 创建并初始化服务器套接字
*
**********************************************************/
int listenFd;
/* 创建套接字 */
listenFd = SockAPI::Socket(SOCK_STREAM);
/* 绑定端口 */
SockAPI::Bind(listenFd, TCP_SERVER_PORT);
/* 开始监听绑定的端口 */
SockAPI::Listen(listenFd);
// Value for select
int fd, maxfd, nready;
fd_set rset, allset;
struct timeval timeout;
// 初始化
maxfd = listenFd;
// allset 用来保存清除完标志的 fd_ret 信息,在每次处理完成后,赋值给 rset
FD_ZERO(&allset);
FD_SET(listenFd, &allset);
int connFd;
string out_ip;
int out_port;
while(1)
{
rset = allset; // rset和allset的搭配使得新加入的fd要等到下次select才会被监听
// 如果有 timeout 设置,那么在每次select之前都要重新设置一下timeout的值
timeout.tv_sec = 0;
timeout.tv_usec = 500000;
nready = select(maxfd + 1, &rset, (fd_set*)NULL, (fd_set*)NULL, &timeout);
if(nready < 0)
{
perror("select error...\n");
exit(-1);
}
else if(nready == 0) // 超时
{
printf(".");
fflush(stdout);
continue;
}
// 接收到请求后,检测数据请求的套接字来自哪些套接字
// 首先检测服务器监听套接字有没有数据
// 如果有的话说明监听到了客户端
// 应该调用 accept 来获取客户端的连接
// 接着检测客户端的连接套接字有没有数据连接
// 如果有的话,说明客户端跟服务器有通信需求
// 首先检测服务器监听套接字有没有数据
if(FD_ISSET(listenFd, &rset))
{
connFd = SockAPI::Accept(listenFd, out_ip, out_port);
// 新加入的描述符,还没判断是否可读写
// 将缓存的 allset 的对应 connFd置位,下次循环时即可监听 connFd
FD_SET(connFd, &allset);
if(connFd > maxfd)
maxfd = connFd;
//到此,监听套接字的信息已经被处理,应该先清除对应位
FD_CLR(listenFd, &rset);
if(--nready <= 0) // nready(用来辅助计数),这样就不(用遍历整个数组)
continue;
}
pid_t pid;
// 遍历套接字看哪些客户端连接套接字有数据请求
for(fd = 0; fd <= maxfd && nready > 0; fd++)
{
if(FD_ISSET(fd, &rset)) // 检测到客户端连接套接字 fd 有数据请求
{
// 单进程的环境下,不可以阻塞在这里,可以选择非阻塞
pid = fork();
if(pid < 0)
{
perror("fork error");
exit(-1);
}
else if(pid == 0)
{
printf("分叉了一个新的进程%d用于处理客户端的连接\n", getpid());
SockAPI::Close(listenFd);
printf("\n开始与客户端通信,套接字描述符 fd = %d\n", fd);
Server::RaiseClientRequest(fd, out_ip);
// 清除 allset 的对应位,以备 fd 可以被 select 继续监听
FD_CLR(fd, &allset);
}
}
}
}
return EXIT_SUCCESS;
}
select 模式的多线程非阻塞 accept 服务器
server.cc
/**********************************************************
> File Name: server.c
> Author: Darwlr
> Created Time: 2020年08月17日 星期一 12时22分10秒
*********************************************************/
#include "server.h"
#define TCP_SERVER_PORT 6666 /* 服务器的端口 */
typedef struct{
int connFd;
string out_ip;
}param_t;
void *threadFunc(void *args)
{
pthread_detach(pthread_self());
param_t *p = (param_t*)args;
printf("分叉了一个新的线程%ld用于处理客户端的连接\n", pthread_self());
Server::RaiseClientRequest(p->connFd, p->out_ip);
return NULL;
}
extern int errno;
int main(int argc, char *argv[])
{
/**********************************************************
*
* 创建并初始化服务器套接字
*
**********************************************************/
int listenFd;
/* 创建套接字 */
listenFd = SockAPI::Socket(SOCK_STREAM);
/* 绑定端口 */
SockAPI::Bind(listenFd, TCP_SERVER_PORT);
/* 开始监听绑定的端口 */
SockAPI::Listen(listenFd);
// Value for select
int fd, maxfd, nready;
fd_set rset, allset;
struct timeval timeout;
// 初始化
maxfd = listenFd;
// allset 用来保存清除完标志的 fd_ret 信息,在每次处理完成后,赋值给 rset
FD_ZERO(&allset);
FD_SET(listenFd, &allset);
int connFd;
string out_ip;
int out_port;
while(1)
{
rset = allset; // rset和allset的搭配使得新加入的fd要等到下次select才会被监听
// 如果有 timeout 设置,那么在每次select之前都要重新设置一下timeout的值
timeout.tv_sec = 0;
timeout.tv_usec = 500000;
nready = select(maxfd + 1, &rset, (fd_set*)NULL, (fd_set*)NULL, &timeout);
if(nready < 0)
{
perror("select error...\n");
exit(-1);
}
else if(nready == 0) // 超时
{
printf(".");
fflush(stdout);
continue;
}
// 接收到请求后,检测数据请求的套接字来自哪些套接字
// 首先检测服务器监听套接字有没有数据
// 如果有的话说明监听到了客户端
// 应该调用 accept 来获取客户端的连接
// 接着检测客户端的连接套接字有没有数据连接
// 如果有的话,说明客户端跟服务器有通信需求
// 首先检测服务器监听套接字有没有数据
if(FD_ISSET(listenFd, &rset))
{
connFd = SockAPI::Accept(listenFd, out_ip, out_port);
// 新加入的描述符,还没判断是否可读写
// 将缓存的 allset 的对应 connFd置位,下次循环时即可监听 connFd
FD_SET(connFd, &allset);
if(connFd > maxfd)
maxfd = connFd;
//到此,监听套接字的信息已经被处理,应该先清除对应位
FD_CLR(listenFd, &rset);
if(--nready <= 0) // nready(用来辅助计数),这样就不(用遍历整个数组)
continue;
}
pthread_t tid;
// 遍历套接字看哪些客户端连接套接字有数据请求
for(fd = 0; fd <= maxfd && nready > 0; fd++)
{
if(FD_ISSET(fd, &rset)) // 检测到客户端连接套接字 fd 有数据请求
{
param_t param;
param.connFd = connFd;
param.out_ip = out_ip;
if(pthread_create(&tid, NULL, threadFunc, (void*)¶m) != 0)
{
perror("thread create error");
}
// 清除 allset 的对应位,以备 fd 可以被 select 继续监听
FD_CLR(fd, &allset);
}
}
}
return EXIT_SUCCESS;
}