Unix网络编程 学习记录02--select和字符串回显程序

前言

在《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的请求返回回显数据。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值