前言
在《unix网络编程》的第四章中,我们简单学习了socket编程的核心函数(connect、bind、listen、accept、close);在第六章中,我们简单学习了IO多路复用的两个核心函数:select和poll。为了巩固知识,我在本篇论文中实现了一个简单的字符串回显网络程序。
服务端
这里我们使用的是select函数,该函数允许进程只是内核,等待多个事件中的任何一个发生,并且在有一鸽或多个事件发生或精力一段时间后才唤醒它。以下是select函数定义。
#include <sys/select.h>
int select (int maxfdp1, fd_set *readset, fd_set *write_set, fd_set *exceptset, const struct timeval *timeout);
/*
* 如果有就绪描述符,返回大于就绪符个数
* 如果超时,返回0
* 如果出错,返回-1
*/
事件的类型有读、写、异常三种,分别对应三个不同的fd_set。fd_set的数据结构是一个long int数组,数组的长度为1024/sizeof(long int)
,每一个long int的每一位对应一个描述符,比如在32位机器中,long的长度为32位,数组长度为1024/32 = 32. 数组的第一个元素对应0~31描述符, 第二个元素对应32~63描述符。如果在64位机器中,long的长度为64位,数组长度为1024/64 = 16. 数组第一个元素对应0~63描述符,以此类推。总之,描述符集合的最大长度为1024。maxfdp1
为最大描述符位+1,select
只会对[0,maxfdp1-1]
范围的描述符进行监听。
为了设置fd_set中的每一位,select.h
提供了以下四个宏函数
void FD_ZERO(fd_set fd_set *fdset); //清除fdset中所有bits
void FD_SET(int fd, fd_set *fdset); //把fdset中的第fd个bit置为1
void FD_CLR(int fd, fd_set *fdset); //把fdset中的第fd个bit置为0
int FD_ISSET(int fd, fd_set *fdset) //判断fdset的第fd个bit是否为1
这些宏函数的使用方法也很简单,在初始化时,我们先对某个集合调用FD_ZERO
,然后将我们需要监听的套接字描述符位通过FD_SET
设置为1。select函数会对fd_set中描述符位为1的描述符集合进行监听,如果没有可读写或异常,该位会被设置位0。在关闭连接后通过FD_CLR
将描述符位设置为0。
由于select()
会对fd_set的描述符位进行更改,所以一般在调用的时候都要拷贝fd_set到一个临时变量,然后用临时变量来作为入参。具体过程参照下图:
这里我简单写了一个字符串回显的demo,定义一个server类。用于将客户端发送过来的字符串返回。
//
// server.h
// Created by 1023198294 on 2021/10/9.
//
#ifndef LAB1_SERVER_H
#define LAB1_SERVER_H
#define MAXLINE 1000
#define LISTENQ 1024
#include <iostream>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <unistd.h>
#include <vector>
#include <cmath>
using namespace std;
class server {
public:
server();
void run();
server(const server&) = delete;
server &operator=(const server&) = delete;
private:
int listen_fd; //监听套接字
char buf[MAXLINE]{}; //接收/发送缓存
fd_set fdsetr{},fdsetw{}; //可读文件描述符集,可写文件描述符集
vector<int> conns; //保存套接字,默认值为-1
};
#endif //LAB1_SERVER_H
头文件中的具体方法实现如下
//
// server.cpp
// Created by 1023198294 on 2021/10/9.
//
server::server() {
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr{};
servaddr.sin_addr.s_addr = htonl((in_addr_t) 0); // 0.0.0.0 -> 操作系统分配默认ip
servaddr.sin_port = htons(5657);
servaddr.sin_family = AF_INET;
int bind_status = bind(listen_fd, (sockaddr *) &servaddr, sizeof(servaddr)); //绑定套接字
if (bind_status < 0) {
std::cout << "error binding socket! errno:" << errno << std::endl;
exit(-1);
}
for (int i = 0; i < FD_SETSIZE; ++i) {
conns.emplace_back(-1);
}
}
void server::run() {
listen(listen_fd, LISTENQ);// 连接就绪队列大小 backlog 设置为1024
FD_ZERO(&fdsetr);
FD_ZERO(&fdsetw);
FD_SET(listen_fd, &fdsetr);
fd_set read_fd;//用于拷贝原来的文件描述符集
fd_set write_fd; //用于保存写的文件描述符集
int maxfd = listen_fd;//select的第一个参数,用于记录最大文件描述符的值+1
int max_index = -1; // 所有客户端中最大的下标
sockaddr_in cli_addr{};//用于保存client信息
size_t cli_size = sizeof(cli_addr);
struct timeval timeout = {3, 0};
while (true) {
read_fd = fdsetr;//拷贝读文件描述符集
write_fd = fdsetw;//拷贝写文件描述符集
int ready_socks = select(maxfd + 1, &read_fd, &write_fd, nullptr, &timeout);
if (ready_socks <= 0)
continue;
if (FD_ISSET(listen_fd, &read_fd)) {
//如果有新的客户端连进来
int connfd = accept(listen_fd, (sockaddr *) &cli_addr, (socklen_t *) &cli_size);
//建立连接套接字
char str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &cli_addr.sin_addr, str, sizeof(str));
std::cout << "new client:" + string(str) + ", port " + to_string(cli_addr.sin_port) + "\n" << std::endl;
bool place = false;
for (int i = 0; i < conns.size(); i++) {
if (conns[i] < 0) {
//寻找可用空间
conns[i] = connfd;
place = true;
max_index = max(i, max_index);
break;
}
}
if (!place) {
std::cout << "too many clients" << std::endl;
exit(-1);
}
FD_SET(connfd, &fdsetr); //增加描述符
if (connfd > maxfd) {
maxfd = connfd;
}
}
int sockfd;
for (int i = 0; i <= max_index; i++) {
if ((sockfd = conns[i]) >= 0) {
//是否可发送
if (FD_ISSET(sockfd, &write_fd)) {
if (strlen(buf) > 0) {
int ret = send(sockfd, buf, MAXLINE, 0);
if (ret > 0) {
std::cout << "send: " << ret << " bytes" << std::endl;
}
FD_CLR(sockfd, &fdsetw);
}
buf[0] = '\0'; //清空
}
//是否可接收
if (FD_ISSET(sockfd, &read_fd)) {
char buf_read[MAXLINE];
int n = recv(sockfd, buf_read, MAXLINE, 0);
if (n < 0) {
//出现错误
if (errno == ECONNRESET) {
//TCP RST
close(sockfd);
FD_CLR(sockfd, &fdsetr);
conns[i] = -1;
std::cout << "connection reset" << std::endl;
} else {
std::cout << "connection error! errno:" << errno << std::endl;
exit(-1);
}
} else if (n == 0) {
//连接断开
std::cout << "0:disconnect" << std::endl;
close(sockfd);
FD_CLR(sockfd, &fdsetr);
conns[i] = -1;
ready_socks--;
} else {
//可接收信息
buf_read[n] = '\0';
std::cout << "recv message:" << buf_read << std::endl;
memcpy(buf, buf_read, strlen(buf_read) + 1);
FD_SET(conns[i], &fdsetw);
}
}
}
}
}
}
然后我们定义主函数
#include <iostream>
#include "include/server.h"
int main() {
std::cout << "Begin Server" << std::endl;
auto *s = new server();
s->run();
return 0;
}
客户端
跟《学习记录01》一样,我们客户端程序在windows上跑,用getline函数从命令行获取输入并发送到服务端,然后打印服务端返回的回显字符串。
#include <iostream>
#include <WinSock2.h>
#include <windows.h>
#include <iostream>
#include <cmath>
#include <unistd.h>
#define MAXLINE 1000
#pragma comment (lib, "ws2_32.lib")
void str_cli(SOCKET sockfd);
int main() {
std::cout << "Begin Client!" << std::endl;
WSADATA wsd;
if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0) {
WSACleanup();
return -1;
}
SOCKET sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == INVALID_SOCKET) {
std::cout << "ERROR: " << &WSAGetLastError << std::endl;
WSACleanup();
return -2;
}
SOCKADDR_IN client;
client.sin_family = AF_INET;
client.sin_port = htons(5657);
client.sin_addr.S_un.S_addr = inet_addr("***.***.***.***");//手动和谐
int con_status = connect(sockfd, (sockaddr *) &client, sizeof(client));
if (con_status < 0) {
std::cout << "ERROR: " << &WSAGetLastError << std::endl;
WSACleanup();
return -1;
}
str_cli(sockfd);
closesocket(sockfd);
WSACleanup();
system("pause");//那个为了测试,没什么卵用
return 0;
}
void
str_cli(SOCKET sockfd) {
char sendline[MAXLINE];
char recvline[MAXLINE];
while (std::cin.getline(sendline, MAXLINE)) {
send(sockfd, sendline, MAXLINE, 0);
int len = recv(sockfd, recvline, MAXLINE, 0);
if (len == 0) {
std::cout << "connection is closed" << std::endl;
break;
}
std::cout << "recv from server: " << recvline << std::endl;
}
}
运行结果
这里我开启了一个服务端,两个客户端,两个客户端交替发出数据。
可以看到,服务器并发地对对多个client的请求返回回显数据。