在B站上看见此视频记录一下,之前并未将socket弄明白,观看之后略懂一二;
【IO多路复用是什么?如何设计一个高性能服务器?】 https://www.bilibili.com/video/BV1WF411R7aK/?share_source=copy_web&vd_source=be53caa3c278a0606cdc59931333e597
【腾讯面试:同步阻塞(BIO)、同步非阻塞(NIO)和Select IO多路复用的原理和不同点】 https://www.bilibili.com/video/BV15X4y1Y7T9/?share_source=copy_web&vd_source=be53caa3c278a0606cdc59931333e597
在服务器中accept()等待客户端连接为阻塞类型,read()也为阻塞接收;
socket()=>bind()=>listen()=>accept()=>read()=>close(accept)=>close(listen)
1、创建一个socket,用函数socket();
可选 2、设置socket属性,用函数setsockopt(); *
3、绑定IP地址、端口等信息到socket上,用函数bind();
4、开启监听连接,用函数listen();
5、接收客户端上来的连接,用函数accept();
6、收发数据,用函数send()和recv(),或者read()和write();
7、关闭客户端连接;
8、关闭监听;
socket()=>connect()=>write()=>close()
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();*
3、绑定IP地址、端口等信息到socket上,用函数bind();*
4、设置要连接的对方的IP地址和端口等属性;
5、连接服务器,用函数connect();
6、收发数据,用函数send()和recv(),或者read()和write();
7、关闭网络连接;
一个简单的 server端,仅能保持单个客户端连接并信息打印;
服务器代码server.cpp
#include
#include
#include
//#include
//#include
using namespace std;
int main()
{
// 创建socket
int isockt_fd = socket(AF_INET, SOCK_STREAM, 0);
if(isockt_fd < 0)
{
cout<< "socket create err ...";
return 0;
}
// 设置端口等信息
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888); // 端口号
addr.sin_addr.s_addr = htons(INADDR_ANY);
// 绑定ip地址
int res = bind(isockt_fd, (struct sockaddr*)& addr, sizeof(addr));
if(res < 0)
{
cout<< "bind err res = "<< res <<endl;
return 0;
}
cout<< "创建成功"<<endl;
// 监听客户端连接
listen(isockt_fd, 30);
struct sockaddr_in client;
socklen_t len = sizeof(client);
// 阻塞等待客户端连接
int fd = accept(isockt_fd, (struct sockaddr*)& client, &len);
if(fd < 0)
{
cout<< "accept err res = " << fd <<endl;
return 0;
}
cout<<"链接成功"<<endl;
char buffer[1024];
// 阻塞方式接收
read(fd, buffer, sizeof(buffer));
cout<<buffer<<endl;
close(fd);
close(isockt_fd);
}
客户端代码client.cpp
#include
#include
#include
#include
#include
using namespace std;
int main()
{
int isock_fd = socket(AF_INET, SOCK_STREAM, 0);
if(isock_fd < 0)
{
cout<< "sockt err ..."<<endl;
return 0;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = inet_addr("10.10.77.47");
int res = connect(isock_fd, (struct sockaddr*)& addr, sizeof(addr));
if(res < 0)
{
cout<< "connect err ..."<<endl;
return 0;
}
cout<< "链接成功"<<endl;
string data;
cout<< "in:";
cin >> data;
write(isock_fd, data.c_str(), data.size());
close(isock_fd);
return 0;
}
server编译:g++ server.cpp -o server
client编译:g++ client.cpp -o client
上述方式由于accept()和read()阻塞只能进行单个连接接收,
那如何使多个客户端对服务器进行连接呢,那我们就需要使用线程对accept()函数进行多次调用链接,同时将read()也进行循环调用,循环接收进行业务处理
服务器代码server.cpp
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
int isockt_fd = socket(AF_INET, SOCK_STREAM, 0);
if (isockt_fd < 0)
{
cout << "socket create err ...";
return 0;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888); // 端口号
addr.sin_addr.s_addr = htons(INADDR_ANY);
int res = bind(isockt_fd, (struct sockaddr *)&addr, sizeof(addr));
if (res < 0)
{
cout << "bind err res = " << res << endl;
return 0;
}
cout << "创建成功" << endl;
// 监听连接请求
listen(isockt_fd, 30);
cout << "监听连接请求" << endl;
// 循环等待新连接accept
while (1)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
// accept 阻塞等待客户端 连接
int fd = accept(isockt_fd, (struct sockaddr *)&client, &len);
// 使用detach()方式将线程立即执行
std::thread([&]()
{
// 需要获取fd的值,不能直接使用fd,(由于Lambda表达式使用&接收后线程都会使用相同的fd描述符)
int fd_thread = fd;
if (fd_thread < 0)
{
cout << "accept err res = " << fd_thread << endl;
return 0;
}
cout << "链接成功" << endl;
while(1)
{
char buffer[1024];
// read阻塞方式接收
int read_len = read(fd_thread, buffer, 1024);
if (read_len == 0) // 断开连接
{
cout<<"断开连接"<<endl;
close(fd_thread);
}
else // 返回字节数 -- 业务逻辑处理
{
cout << buffer << endl;
memset(buffer, '\0', sizeof(buffer)); // 清空数组
}
}
}).detach();
}
close(isockt_fd);
}
编译:g++ server.cpp -o server -lpthread
select函数用于在非阻塞中,当一个套接字或一组套接字有信号时通知你,系统提供select函数来实现多路复用输入/输出模型
#include
#include
int select(int maxfd,fd_set *rdset,fd_set *wrset,fd_set *exset,struct timeval *timeout);
1、maxfd:是需要监视的最大的文件描述符值+1
2、rdset:读文件描述符监听集合,传入、传出参数
3、wrset:写文件描述符监听集合,传入、传出参数
4、exset:异常文件描述符监听集合,传入、传出参数
5、timeout:指向timeval结构体的指针,通过传入的这个timeout参数来决定select()函数的三种执行方式
1.传入的timeout为NULL,则表示将select()函数置为阻塞状态,直到我们所监视的文件描述符集合中某个文件描述符发生变化是,才会返回结果。
2.传入的timeout为0秒0毫秒,则表示将select()函数置为非阻塞状态,不管文件描述符是否发生变化均立刻返回继续执行。
3.传入的timeout为一个大于0的值,则表示这个值为select()函数的超时时间,在timeout时间内一直阻塞,超过时间即返回结果 。
fd_set类型四个宏操作:(fd为文件描述符)
1、void FD_ZERO(fd_set *fdset);将指定的文件描述符集清空,在对文件描述符集合进行设置前,必须对其进行初始化,如果不清空,由于在系统分配内存空间后,通常并不作清空处理,所以结果是不可知的。
2、void FD_SET(int fd, fd_set *fdset); 用于在文件描述符集合中增加一个新的文件描述符。
3、void FD_CLR(int fd, fd_set *fdset);用于在文件描述符集合中删除一个文件描述符。
4、int FD_ISSET(int fd, fd_set *fdset); 用于测试指定的文件描述符是否在该集合中
可以按照下面的思路来理解这些宏:a[maxfd-1], …, a[1], a[0]
FD_ZERO 用来将这个向量的所有元素都设置成 0;
FD_SET 用来把对应套接字 fd 的元素,a[fd]设置成 1;
FD_CLR 用来把对应套接字 fd 的元素,a[fd]设置成 0;
FD_ISSET 对这个向量进行检测,判断出对应套接字的元素 a[fd]是 0 还是 1。
其中 0 代表不需要处理,1 代表需要处理。
其中还需要使用ioctl()
#include
int ioctl(int fd, unsigned long request, ...);
ioctl 是用来设置硬件控制寄存器,或者读取硬件状态寄存器的数值之类的。
ioctl(fd, FIONREAD, &b);
得到缓冲区里有多少字节要被读取,然后将字节数放入b里面。
FIONREAD用于判断接收缓存中是否存在可读数据,用于accept的socket处于LISTEN状态
而read,write 是把数据丢入缓冲区,硬件的驱动从缓冲区读取数据一个个发送或者把接收的数据送入缓冲区。
server.cpp
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
int isocket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (isocket_fd < 0)
{
cout << "socket create err ...";
return 0;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888); // 端口号
addr.sin_addr.s_addr = htons(INADDR_ANY);
int res = bind(isocket_fd, (struct sockaddr *)&addr, sizeof(addr));
if (res < 0)
{
cout << "bind err res = " << res << endl;
return 0;
}
cout << "创建成功" << endl;
// 监听连接请求
listen(isocket_fd, 30);
cout << "监听连接请求" << endl;
fd_set read_fds, test_fds;
FD_ZERO(&read_fds); // 清空数组
FD_SET(isocket_fd, &read_fds); // 将socket添加到集合中
// 循环等待新连接accept
while (1)
{
test_fds = read_fds; // 将需要监视的描述符集copy到select查询队列中,select会对其修改,所以一定要分开使用变量
// FD_SETSIZE:系统默认的最大文件描述符,进行阻塞当accept()有连接时,进行向下触发
select(FD_SETSIZE, &test_fds, NULL, NULL, NULL);
for (int fd = 0; fd < FD_SETSIZE; fd++)
{ // i表示文件描述符
if (FD_ISSET(fd, &test_fds))
{
// 判断是否为socket监听触发
if (fd == isocket_fd)
{
// 触发accept客户端连接
struct sockaddr_in client;
socklen_t len = sizeof(client);
// accept 阻塞等待客户端 连接
int client_fd = accept(isocket_fd, (struct sockaddr *)&client, &len);
if (client_fd < 0)
{
cout << "accept err res = " << client_fd << endl;
return 0;
}
FD_SET(client_fd, &read_fds); // 将客户端fd描述符加入到read_fds集合中
cout << "链接成功" << endl;
}
else // 客户端有数据请求触发
{
/*
得到缓冲区里有多少字节要被读取,然后将字节数放入b里面。
ioctl(fd, FIONREAD, &b);
*/
int read_len;
ioctl(fd, FIONREAD, &read_len); // fd为select中客户端fd描述符,
if (read_len == 0) // 断开连接
{
cout << "断开连接" << endl;
close(fd);
FD_CLR(fd, &read_fds);
}
else if (read_len == -1) // 非阻塞,无数据可读
{
continue;
}
else // 返回字节数 -- 业务逻辑处理
{
char buffer[1024];
int read_len = read(fd, buffer, 1024);
cout << buffer << endl;
memset(buffer, '\0', sizeof(buffer)); // 清空数组
}
}
}
}
}
close(isocket_fd);
}