学习视频链接
目录
一、多路 IO 服务器
1.1 分类
select 模型
poll 模型
epoll 模型
1.2 select 原理图
这里 select 不是等待客户端连接,而是客户端想要连接需要需要通知 select,然后 select 把信息交给主函数,主函数执行 accept(),然后由 select 负责去写数据。涉及到阻塞、非阻塞轮询、响应式——多路 IO 转接,上述例子是多路 IO 转接
二、select 函数
2.1 传入、传出参数
程序启动起来后 0 1 2 这 3 个文件描述符就被占用了。后面的文件描述符 3 被 lfd 占用,用于监听,后面的连接依次占用文件描述符 4、5、6 ...
客户端连接 select 上后没有发送信息的话,就不需要 select 读监听,想监听的话就把对应的文件描述符放入读集合 (r) 就可以了。写和异常的监听放在另外两个集合里面
现阶段没有这两种需求,所有传入 NULL 就行了
2.2 select函数和后续分析
1、函数
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds: 监控的文件描述符集里最大文件描述符加 1,因为此参数会告诉内核检测前多少个文件描述符的状态
readfds: 监控有读数据到达文件描述符集合,传入传出参数
writefds: 监控写数据到达文件描述符集合,传入传出参数
exceptfds: 监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
timeout: 定时阻塞监控时间,3种情况。
(1) NULL,永远等下去
(2) 设置 timeval,等待固定时间。
(3) 设置 timeval 里时间均为 0,检查描述字后立即返回,轮询
2、分析
select 函数最后一个参数设置轮询的时间,在监听期间有发送信息的话,会放入到传出参数中,传出参数同样使用位图
一开始监听 3、5、6 文件描述符对应的读 (r),3 在监听的时候没有读 (r),所以传出中的位图没有 3 了
2.3 配套函数
void FD_ZERO(fd_set *set); —— 清空一个文件描述符集合
fd_set rset;
FD_ZERO(&rset);
void FD_SET(int fd, fd_set *set); —— 将待监听的文件描述符,添加到监听集合中
FD_SET(3, &rset);
FD_SET(5, &rset);
FD_SET(6, &rset);
void FD_CLR(int fd, fd_ set *set); —— 将一个文件描述符从监听集合中移除
FD_CLR(4, &rset);
int FD_ISSET(int fd, fd_set *set); —— 判断一个文件描述符是否在监听集合中
返回值: 在返回1;不在返回0
FD_ISSET(4, &rset);
2.4 代码
进入 while 前,监听的位图 allset 就是上图这个,maxfd 对应的值是 3
#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#define SERV_PORT 6666
void sys_err(const char* str)
{
perror(str);
exit(1);
}
int main(void)
{
int i, j, n, nready;
int maxfd = 0;
int listenfd, connfd;
char buf[BUFSIZ];
struct sockaddr_in clie_addr, serv_addr;
socklen_t clie_addr_len;
listenfd = socket(AF_INET, SOCK_STREAM, 0); // 创建一个socket, 得到lfd
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 设置端口复用
bzero(&serv_addr, sizeof(serv_addr)); // 地址结构清零
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 指定本地任意IP
serv_addr.sin_port = htons(SERV_PORT); // 指定端口号
bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); // 绑定
listen(listenfd, 128); // 设置同一时刻链接服务器上限数
fd_set rset, allset; // rset读事件文件描述符 allset用来暂存
maxfd = listenfd;
FD_ZERO(&allset);
FD_SET(listenfd, &allset); // 构造select监控文件描述符集
while (1) {
rset = allset; // 每次循环时都要重新设置select监控信号集
nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
if (nready < 0) {
sys_err("select error");
}
if (FD_ISSET(listenfd, &rset)) { // 说明有新的客户端链接请求
clie_addr_len = sizeof(clie_addr);
connfd = accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len); // accept不会阻塞
FD_SET(connfd, &allset); // 向监控文件描述符集合allset添加新的文件描述法connfd
if (maxfd < connfd) {
maxfd = connfd;
}
if (0 == --nready) {
continue;
}
}
for (i = listenfd + 1; i <= maxfd; i++) { // 检测哪个客户端有数据就绪
if (FD_ISSET(i, &rset)) {
if ((n = read(i, buf, sizeof(buf))) == 0) { // 当客户端关闭链接时,服务器也关闭对应链接
close(i);
FD_CLR(i, &allset);
}
else if (n > 0) {
for (j = 0; j < n; j++) {
buf[j] = toupper(buf[j]);
}
write(i, buf, n);
}
}
}
}
close(listenfd);
return 0;
}
2.5 linux和windows通信
windows 代码
#define WIN32_LEAN_AND_MEAN
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream>
#include <WinSock2.h>
#include <windows.h>
#pragma comment(lib, "ws2_32.lib")
using namespace std;
int main()
{
WORD ver = MAKEWORD(2, 2); // 版本号,启动windows socket 2.x环境
WSADATA dat; // 数据结构,用来存储被WSAStartup函数调用后返回的Windows Sockets数据。
// 它包含Winsock.dll执行的数据。
WSAStartup(ver, &dat);
// 1.建立一个socket
SOCKET _sock = socket(AF_INET, SOCK_STREAM, 0);
// 2.连接服务器
sockaddr_in _sin = {};
_sin.sin_family = AF_INET;
_sin.sin_port = htons(6666);
_sin.sin_addr.S_un.S_addr = inet_addr("192.168.244.137");
int ret = connect(_sock, (sockaddr*)&_sin, sizeof(sockaddr_in));
if (SOCKET_ERROR == ret) {
cout << "绑定网络端口失败" << endl;
}
else {
cout << "绑定网络端口成功" << endl;
}
char recvBuf[256] = {};
int nlen = recv(_sock, recvBuf, 256, 0);
if (nlen > 0) {
cout << "接收到数据: " << recvBuf << endl;
}
// 4.关闭套接字closesocket
closesocket(_sock);
getchar();
// 清除windows socket环境
WSACleanup();
return 0;
}
拓展一下思路,其他的 TCP 通信同样可以和服务端连接
2.6 select的优缺点
1、缺点
监听上限受文件描述符限制,最大 1024 个
检测满足条件的 fd,自己添加业务逻辑提高小。提高了编码难度。
2、优点
跨平台
2.7 添加一个自己定义数组提高效率
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <ctype.h>
#include "wrap.h"
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
int i, j, n, maxi;
int nready, client[FD_SETSIZE]; // 自定义数组client, 防止遍历1024个文件描述符 FD_SETSIZE默认为1024 */
int maxfd, listenfd, connfd, sockfd;
char buf[BUFSIZ], str[INET_ADDRSTRLEN]; // #define INET_ADDRSTRLEN 16
struct sockaddr_in clie_addr, serv_addr;
socklen_t clie_addr_len;
fd_set rset, allset; // rset 读事件文件描述符集合 allset用来暂存
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family= AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port= htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
Listen(listenfd, 128);
maxfd = listenfd; // 起初 listenfd 即为最大文件描述符
maxi = -1; // 将来用作client[]的下标, 初始值指向0个元素之前下标位置
for (i = 0; i < FD_SETSIZE; i++) {
client[i] = -1; // 用-1初始化client[]
}
FD_ZERO(&allset);
FD_SET(listenfd, &allset); // 构造select监控文件描述符集
while (1) {
rset = allset; // 每次循环时都重新设置select监控信号集
nready = select(maxfd+1, &rset, NULL, NULL, NULL); // 2 1--lfd 1--connfd
if (nready < 0) {
perr_exit("select error");
}
if (FD_ISSET(listenfd, &rset)) { // 说明有新的客户端链接请求
clie_addr_len = sizeof(clie_addr);
connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len); // Accept 不会阻塞 */
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
ntohs(clie_addr.sin_port));
for (i = 0; i < FD_SETSIZE; i++)
if (client[i] < 0) { // 找client[]中没有使用的位置
client[i] = connfd; // 保存accept返回的文件描述符到client[]里
break;
}
if (i == FD_SETSIZE) { // 达到select能监控的文件个数上限 1024
fputs("too many clients\n", stderr);
exit(1);
}
FD_SET(connfd, &allset); // 向监控文件描述符集合allset添加新的文件描述符connfd
if (connfd > maxfd)
maxfd = connfd; // select第一个参数需要
if (i > maxi) {
maxi = i; // 保证maxi存的总是client[]最后一个元素下标
}
if (--nready == 0) {
continue;
}
}
for (i = 0; i <= maxi; i++) { // 检测哪个clients 有数据就绪
if ((sockfd = client[i]) < 0) {
continue;
}
if (FD_ISSET(sockfd, &rset)) {
if ((n = Read(sockfd, buf, sizeof(buf))) == 0) { // 当client关闭链接时,服务器端也关闭对应链接 */
Close(sockfd);
FD_CLR(sockfd, &allset); // 解除select对此文件描述符的监控
client[i] = -1;
}
else if (n > 0) {
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
Write(sockfd, buf, n);
Write(STDOUT_FILENO, buf, n);
}
if (--nready == 0) {
break; // 跳出for, 但还在while中
}
}
}
}
Close(listenfd);
return 0;
}
wrap.h
#ifndef WRAP_H
#define WRAP_H
#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>
void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
void Bind(int fd, const struct sockaddr *sa, socklen_t salen);
void Connect(int fd, const struct sockaddr *sa, socklen_t salen);
void Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
void Close(int fd);
ssize_t Readline(int fd,void *vptr,size_t maxlen);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
#endif
wrap.c
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include "sys/socket.h"
#include "ctype.h"
#include "arpa/inet.h"
#include "wrap.h"
void perr_exit(const char *s)
{
perror(s);
exit(1);
}
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
int n;
again:
if((n = accept(fd, sa, salenptr)) < 0) {
if((ECONNABORTED == errno) || (EINTR == errno))
goto again;
else
perr_exit("accept error");
}
return n;
}
void Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
if(bind(fd, sa, salen) < 0)
perr_exit("bind error");
}
void Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
if(connect(fd, sa, salen) < 0)
perr_exit("connect error");
}
void Listen(int fd, int backlog)
{
if(listen(fd, backlog) < 0)
perr_exit("listen error");
}
int Socket(int family, int type, int protocol)
{
int n;
if((n = socket(family, type, protocol)) < 0)
perr_exit("socket error");
return n;
}
ssize_t Read(int fd, void *ptr, size_t nbytes)
{
ssize_t n;
again:
if((n = read(fd, ptr, nbytes)) == -1) {
if(EINTR == errno)
goto again;
else
return -1;
}
return n;
}
ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
ssize_t n;
again:
if((n = write(fd, ptr, nbytes)) == -1) {
if(EINTR == errno)
goto again;
else
return -1;
}
return n;
}
void Close(int fd)
{
if(close(fd) == -1)
perr_exit("close error");
}
ssize_t Readn(int fd, void *vptr, size_t nbytes)
{
size_t nleft;
size_t nread;
char *ptr;
ptr = vptr;
nleft = nbytes;
while(nleft > 0) {
if((nread = read(fd, ptr, nleft)) < 0) {
if(EINTR == errno)
nread = 0;
else
return -1;
} else if(nread == 0)
break;
nleft -= nread;
ptr += nread;
}
return (nbytes-nleft);
}
static ssize_t my_read(int fd,char *ptr)
{
static int read_cnt;
static char * read_ptr;
static char read_buf[100];
if(read_cnt <= 0){
again:
if((read_cnt == read(fd,read_buf,sizeof(read_buf))) < 0){
if(errno == EINTR)
goto again;
return -1;
}else if(read_cnt == 0)
return 0;
read_ptr = read_buf;
}
read_cnt--;
*ptr = *read_ptr++;
return 1;
}
ssize_t Readline(int fd,void *vptr,size_t maxlen)
{
ssize_t n,rc;
char c,*ptr;
ptr = vptr;
for(n = 1;n < maxlen;n ++){
if((rc = my_read(fd,&c)) == 1)
{
*ptr++ = c;
if(c == '\n')
break;
}else if(rc == 0){
*ptr = 0;
return n - 1;
}else
{
return -1;
}
}
*ptr = 0;
return n;
}
ssize_t Writen(int fd, const void *vptr, size_t nbytes)
{
size_t nleft;
size_t nwritten;
const char *ptr;
ptr = vptr;
nleft = nbytes;
while(nleft > 0) {
if((nwritten = write(fd, ptr, nleft)) <= 0) {
if(nwritten < 0 && EINTR == errno)
nwritten = 0;
else
return -1;
}
nleft -= nwritten;
ptr += nwritten;
}
return nbytes;
}