TCP/IP网络编程-C++ (下)

TCP/IP网络编程-C++ (下)

一、基于Linux的进阶服务端

1、shutdown() 函数 - 优雅的断开连接

1.1、函数原型

cpp 复制代码
	int shutdown(int sock, int howto);
	// 成功时返回0, 失败时返回-1

1.2 参数解析

参数 参数解析
sock 需要断开连接的套接字描述符
howto 传递端口方式信息
  • 第二个参数howto可传递的值如下:
howto可传递值 说明
SHUT_RD 断开输入流,套接字无法再接收数据
SHUT_WR 断开输出流,套接字无法再发送数据
SHUT_RDWR 断开输入流和输出流,套接字无法接受和发送数据
  • 如果shutdown()第二个参数传递"SHUT_RDWR"参数,这样和close()函数有什么区别呢?区别在于使用shutdown()的"SHUT_RDWR"方式关闭,只是会关闭发送和接收数据的功能,并不会关闭套接字描述符,也就是说套接字资源并没有被释放。而close()函数不仅关闭了发送和接收数据的功能,同时关闭了套接字,释放了相关资源。

1.3 shutdown() 函数在client端应用示例代码:

cpp 复制代码
/*头文件和之前示例中一样,故省略*/
const int BUFFER_SIZE = 128;
int main(void){
/*
这和字符串转换功能一样,这里值添加了shutdown()函数调用,故省略一些重复代码。
*/
    while(true){
        std::string str_input = "";
        std::cout<<"please input:";
        std::cin>>str_input;
        if(str_input == "q"){
            if(shutdown(clie_sock, SHUT_RD) == -1){
                std::cout<<"shutdown error\n";
            }
            // 调用了shtudown()后关闭了输入流,也就是不能再接收数据了,但还可以发送数据
            send(clie_sock, str_input.c_str(), str_input.size(), 0);
            break;
        }
        send(clie_sock, str_input.c_str(), str_input.size(), 0);
        
        char message[BUFFER_SIZE] = {0};
        int recv_size = 0;
        if((recv_size = recv(clie_sock, message, BUFFER_SIZE - 1, 0)) == -1){
            std::cout<<"read error\n";
            break;
        }
    }
    close(clie_sock);
    return 0; 
}

2、IP地址和域名相互转换

2.1、gethostbyname() - 域名转换为IP

  • 函数原型
cpp 复制代码
#include <netdb.h>
struct hostent* gethostbyname(const char* hostname);
// 成功返回hostent地址,失败返回NULL.
  • hostent结构体定义如下:
cpp 复制代码
struct hostent
{
  char *h_name;			/* Official name of host.  */
  char **h_aliases;		/* Alias list.  */
  int h_addrtype;		/* Host address type.  */
  int h_length;			/* Length of address.  */
  char **h_addr_list;	/* List of addresses from name server.  */
};
  • 结构体成员说明:
hostent结构体成员 说明
h_name 存放官方域名
h_aliases 存放除官方域名外的其它域名,可以通过这些域名访问同一主页
h_addrtype 保存IP的地址族信息,若是IPv4则保存AF_INET,若是IPv6则保存PF_INET6
h_length 保存IP地址长度
h_addr_list 以整数形式保存域名对应的IP地址
  • gethostbyname()函数代码示例:
cpp 复制代码
#include <iostream>
#include <netdb.h>
#include <arpa/inet.h>
int main(void){
    std::string domain_name = "www.baidu.com";
    struct hostent * host = gethostbyname(domain_name.c_str());
    if(host == NULL){
        std::cout<<"no domain name\n";
        return 0;
    }
    if(host->h_addrtype == AF_INET){
        std::cout<<"use IPv4\n";
    }else if(host->h_addrtype == AF_INET6){
        std::cout<<"use IPv6\n";
    }else{
        std::cout<<"use other\n";
    }
    
    for(int i = 0; host->h_addr_list[i]; ++i){
        std::string ip(inet_ntoa(*(struct in_addr*)host->h_addr_list[i]));
        std::cout<<"ip "<<i<<" is: "<<ip<<"\n";
    }
    return 0;
}
// run result:
// use IPv4
// ip 0 is: 110.242.68.3
// ip 1 is: 110.242.68.4

2.2 gethostbyaddr() - IP转换为域名

  • 函数原型:
cpp 复制代码
#include <netdb.h>
struct hostent* gethostbyaddr(const char* addr, socklen_t len, int family);
// 成功返回hostent地址,失败返回NULL.
  • 参数解析:
参数 参数解析
addr 含有IP地址信息的in_addr结构体指针。为了同时传递IP地址信息外的其它信息,该变量声明为char*类型
len 地址信息的字节数,IPv4时为4,IPv6时为16
family 地址族信息
  • gethostbyaddr()函数代码示例:
cpp 复制代码
#include <iostream>
#include <netdb.h>
#include <arpa/inet.h>
#include <string.h>
int main(void){
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    struct hostent* host = gethostbyaddr(&addr.sin_addr, sizeof(addr.sin_addr), AF_INET);
    if(host == NULL){
        std::cout<<"not found\n";
        return 0;
    }
    for(int i = 0; host->h_aliases[i]; ++i){
        std::string domain_name(host->h_aliases[i]);
        std::cout<<"domain name:"<<domain_name<<"\n";
    }
    return 0;
}
// run result:
// domain name:localhost.localdomain

3、select()函数实现I/O复用服务器

3.1、select()函数介绍

  • 函数原型:
cpp 复制代码
#include <sys/time.h>
#include <sys/select.h>
int select(int maxfd, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timeval* timeout);
// 成功时返回大于0的值,失败时返回-1
  • 参数解析:
参数 参数解析
maxfd 监视对象文件描述符数量
readset 将所有关注"是否存在待读取数据"的文件描述符注册到fd_set变量,并传递其地址值
writeset 将所有关注"是否可传输无阻塞数据"的文件描述符注册到fd_set变量,并传递其地址值
exceptset 将所有关注"是否发生异常"的文件描述符注册到fd_set变量,并传递其地址值
timeout 为防止select()函数陷入无限阻塞状态,所以传递timeout,到达时间后没有监视到变化也会返回
返回值 发生错误时返回-1,超时时返回0(就是达到了timeout时间还未监视到变化),关注的事件发生变化时返回发生变化的文件描述符数量
  • fd_set类型说明:
    (1)、fd_set定义原型
cpp 复制代码
typedef struct{
	long int fds_bits[1024];
}fd_set;

(2)、fd_set就一个long int类型,大小为1024的数组,其下标就表示套接字的文件描述符,所有可以同时监视1024个套接字。每一位都初始化为0,如果某位值为1,则表示监视该套接字。如果下标为3的位值为1,表示监视文件描述符为3的套接字。

(3)、操作fd_set的宏

cpp 复制代码
FD_ZERO(fd_set* fdset);				// 将fd_set所有位初始化为0
FD_SET(int fd, fd_set* fdset);		// 在fd_set中注册文件描述符fd的信息,表示监视文件描述符为fd的套接字
FD_CLR(int fd, fd_set* fdset);		// 在fd_set中清楚文件描述符fd的信息,表示不在监视描述符为fd的套接字
FD_ISSET(int fd, fd_set* fdset);	// 判断fd_set中描述符fd是否被注册,若注册返回true,反之返回false,
  • timeval参数说明:
    (1)、原型及成员说明:
cpp 复制代码
struct timeval{
	long tv_sec;	// 设置秒
	long tv_usec;	// 设置毫秒
}
  • select()函数功能

使用select()函数时可以将多个文件描述符集中到一起监视,会关注如下变化:

(1)、是否存在套接字接收数据

(2)、无需阻塞传输数据的套接字有哪些

(3)、哪些套接字发生了异常

  • select()函数调用顺序

第一步:

1、设置文件描述符:利用fd_set注册文件描述符

2、指定监视范围:范围就是套接字文件描述符最大值+1,也就是select()函数的第一个参数

3、设置超时:将超时时间填在timeout中。

第二步:

1、调用select()函数监听事件

第三步:

1、查看返回结果,获取变化的文件描述符进行相应处理

3.2、I/O复用服务器代码示例:

  • 此示例基于以前的字符串转换的服务端修改的,客户端没有变化,可以使用之前的客户端来验证测试。
cpp 复制代码
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string>
#include <unistd.h>
#include <string.h>
#include <regex>
#include <cctype>
const int BUFFER_SIZE = 128;

void to_lower(const std::string& str_input, std::string& str_output){
    std::regex pattern("^[a-zA-Z]+$");
    bool is_letters = std::regex_match(str_input, pattern);
    if(is_letters){
        str_output.resize(str_input.size());
        for(const auto& da : str_input){
            str_output += std::tolower(da);
        }
    }else{
        str_output = "包含其它字符,转换失败!";
    }
    return;
}

int main(void){
    int serv_sock = socket(PF_INET, SOCK_STREAM, 0);  
    if(serv_sock == -1)
        std::cout<<"socket error\n";

    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(8080);

    if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
        std::cout<<"bind error\n";

    if(listen(serv_sock, 3) == -1)
        std::cout<<"listen error\n";

    struct sockaddr_in clie_addr;
    memset(&clie_addr, 0, sizeof(clie_addr));
    socklen_t clie_addr_size = 0;
    fd_set fd_read, fd_temp;
    FD_ZERO(&fd_read);
    FD_SET(serv_sock, &fd_read);
    int fd_max = serv_sock;
    struct timeval timeout;

    while(true){
        timeout.tv_sec = 5;
        timeout.tv_usec = 0;
        fd_temp = fd_read;
        int fd_num = select(fd_max + 1, &fd_temp, 0, 0, &timeout);
        if(fd_num == -1){
            std::cout<<"select error\n";
            break;
        }else if(fd_num == 0){
            std::cout<<"select timeout\n";
            continue;
        }
        for(int index = 0; index < fd_max + 1; ++index){
            if(FD_ISSET(index, &fd_temp)){
                if(index == serv_sock){
                    int clie_sock = accept(serv_sock, (struct sockaddr*) &clie_addr, &clie_addr_size);
                    if(clie_sock == -1){
                        std::cout<<"accept error\n";
                        break;
                    }
                    FD_SET(clie_sock, &fd_read);
                    fd_max = fd_max > clie_sock ? fd_max : clie_sock;
                }else{
                    char mess[BUFFER_SIZE] = {0};
                    int recv_size = 0;
                    if((recv_size = recv(index, mess, BUFFER_SIZE - 1, 0)) == -1) {
                        std::cout<<"recv error\n";
                        break;
                    }
                    mess[recv_size] = '\0';
                    std::string str_message(mess, recv_size);
                    if(str_message.empty()){
                        FD_CLR(index, &fd_read);
                        close(index);
                    }else{
                        std::string str_result = "";
                        to_lower(str_message, str_result);   
                        send(index, str_result.c_str(), str_result.size(), 0);
                    }
                }
            }
        }
    }
    close(serv_sock);
    return 0;
}

4、高级I/O - readv() & writev() 函数

  • 通过writev()函数可以将分散保存在多个缓冲中的数据一并发送,通过readv()函数可在多个缓冲器分别接收数据。适当的调用这两个函数可以减少I/O函数的调用次数。

4.1、writev()函数介绍

  • 函数原型:
cpp 复制代码
#include <sys/uio.h>
ssize_t writev(int filedes, const struct iovec* iov, int iovcnt);
// 成功时返回发送的字节数,失败时返回-1
  • 参数解析:
参数 参数解析
filedes 套接字文件描述符
iov iovec结构体 数组 的地址,iovec结构体包含待发送的数据和大小
iovcnt iov参数的长度
  • struct iovec结构体原型如下:
cpp 复制代码
struct iovec{
	void* iov_base;	//缓冲地址
	size_t iov_len;	//缓冲大小
}
  • writev()函数代码示例:
cpp 复制代码
#include <sys/uio.h>
#include <iostream>
int main_(void){
    int data_vec_size = 2;
    struct iovec data_vec[data_vec_size];

    char buf_1[] = "hello";
    char buf_2[] = "world";
    data_vec[0].iov_base = buf_1;
    data_vec[0].iov_len = 5;
    data_vec[1].iov_base = buf_2;
    data_vec[1].iov_len = 5;

    int bytes = writev(1, data_vec, data_vec_size);
    if(bytes == -1){
        std::cout<<"writev error\n";
    }
    return 0;
}

4.2、readv()函数介绍

  • 函数原型:
cpp 复制代码
#include <sys/uio.h>
ssize_t readv(int filedes, const struct iovec* iov, int iovcnt);
// 成功时返回发送的字节数,失败时返回-1
  • readv()函数代码示例:
cpp 复制代码
#include <sys/uio.h>
#include <iostream>
#include <string>
int main(void){
    int data_vec_size = 2;
    struct iovec data_vec[data_vec_size];

    char buf_1[128] = {0};
    char buf_2[128] = {0};
    data_vec[0].iov_base = buf_1;
    data_vec[0].iov_len = 128;
    data_vec[1].iov_base = buf_2;
    data_vec[1].iov_len = 128;

    int bytes = readv(1, data_vec, data_vec_size);
    if(bytes == -1){
        std::cout<<"readv error\n";
    }

    std::string str_buf_1(buf_1, 128);
    std::string str_buf_2(buf_2, 128);
    std::cout<<"buf_1:"<<str_buf_1<<"\n";
    std::cout<<"buf_2:"<<str_buf_2<<"\n";
    return 0;
}

5、优与select的epoll

5.1、epoll_create() 函数 - 创建保存epoll文件描述符的空间

  • 函数原型:
cpp 复制代码
#include <sys/epoll.h>
int epoll_create(int size);
// 成功时返回epoll文件描述符,失败时返回-1
// size:epoll实例的大小
  • 调用epoll_create()函数时创建的文件描述符保存空间称为"epoll例程",通过参数size传递的值决定"epoll例程"的大小,但该值并不能决定"epoll例程"的大小,而是仅供操作系统参考。

5.2、epoll_ctl() 函数- 向空间注册并注销文件描述符

  • 函数原型:
cpp 复制代码
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
// 成功时返回0, 失败时返回-1
  • 参数解析:
参数 参数解析
epfd 用于注册监视对象的epoll例程的文件描述符
op 用于指定监视对象的添加、删除或更改等操作
fd 需要注册的监视对象文件描述符
event 监视对象的事件类型
  • 调用示例:

epoll_ctl(A, EPOLL_CTL_ADD, B, C);

含义:epoll例程A中注册文件描述符B,主要目的是监视参数C中的事件
epoll_ctl(A, EPOLL_CTL_DEL, B, NULL);

含义:从例程A中删除文件描述符B

  • epoll_ctl()第二个参数op传递的常量及含义:
含义
EPOLL_CTL_ADD 将文件描述符注册到epoll例程
EPOLL_CTL_DEL 从epoll例程中删除文件描述符
EPOLL_CTL_MOD 更改注册的文件描述的关注事件发生情况
  • epoll_ctl()第四个参数event结构
cpp 复制代码
struct epoll_event{
		__uint32_t events;
		epoll_data_t data;
}
		typedef union epoll_data{
			void* ptr;
			int fd;
			__uint32_t u32;
			__uint64_t u64;
		}eooll_data_t;
  • 其中epoll_enent结构体中成员events可传递的常量及事件如下:
事件
EPOLLIN 读取数据事件
EPOLLOUT 输出缓冲为空,可以立即发送数据的事件
EPOLLPRI 收到OOB数据情况
EPOLLRDHUP 断开连接或半连接的情况,在边缘触发方式下非常有用
EPOLLERR 发生错误的情况
EPOLLET 以边缘触发的方式得到事件通知
EPOLLONESHOT 发生一次事件后,文件描述符不在收到事件通知,需要再次设置事件

5.3、epoll_wait() 函数 - 等待文件描述符发生变化

  • 函数原型:
cpp 复制代码
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
// 成功时返回发生事件的描述符数量,失败返回-1,超时返回0
  • 参数解析
参数 参数解析
epfd 表示事件发生监视范围的epoll例程的文件描述符
events 保存发生事件的文件描述符集合的结构体地址,其所需要的缓冲要使用malloc()动态分配
maxevents 第二个参数可保存的最大事件数
timeout 以毫秒为单位的等待时间,传递-1时会一直等待直到事件发生

5.4、基于epoll的字符串转换服务端代码实现

  • 此示例基于使用select()服务端修改,客户端没有变化,使用之前的服务端即可测试
cpp 复制代码
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string>
#include <unistd.h>
#include <string.h>
#include <regex>
#include <cctype>
#include <sys/epoll.h>
#include <stdlib.h>
const int BUFFER_SIZE = 128;
const int EPOLL_SIZE = 50;

void to_lower(const std::string& str_input, std::string& str_output){
    std::regex pattern("^[a-zA-Z]+$");
    bool is_letters = std::regex_match(str_input, pattern);
    if(is_letters){
        str_output.resize(str_input.size());
        for(const auto& da : str_input){
            str_output += std::tolower(da);
        }
    }else{
        str_output = "包含其它字符,转换失败!";
    }
    return;
}

int main(void){

    int serv_sock = socket(PF_INET, SOCK_STREAM, 0);  
    if(serv_sock == -1)
        std::cout<<"socket error\n";

    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(8080);

    if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
        std::cout<<"bind error\n";

    if(listen(serv_sock, 3) == -1)
        std::cout<<"listen error\n";

    struct sockaddr_in clie_addr;
    memset(&clie_addr, 0, sizeof(clie_addr));
    socklen_t clie_addr_size = 0;
    int epfd = epoll_create(EPOLL_SIZE);
    struct epoll_event *ep_events = (struct epoll_event*)malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
    struct epoll_event events;
    events.events = EPOLLIN;
    events.data.fd = serv_sock;
    epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &events);

    while(true){
        int event_count = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
        if(event_count == -1){
            std::cout<<"epoll error\n";
            break;
        }
        for(int index = 0; index < event_count; ++index){
            if(ep_events[index].data.fd == serv_sock){
                int clie_sock = accept(serv_sock, (struct sockaddr*) &clie_addr, &clie_addr_size);
                if(clie_sock == -1){
                    std::cout<<"accept error\n";
                    break;
                }
                events.events = EPOLLIN;
                events.data.fd = clie_sock;
                epoll_ctl(epfd, EPOLL_CTL_ADD, clie_sock, &events);
            }else{
                char mess[BUFFER_SIZE] = {0};
                int recv_size = 0;
                if((recv_size = recv(ep_events[index].data.fd, mess, BUFFER_SIZE, 0)) == -1) {
                    std::cout<<"recv error\n";
                    break;
                }
                mess[recv_size] = '\0';
                std::string str_message(mess, recv_size);
                if(str_message.empty()){
                    epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[index].data.fd, NULL);
                    close(ep_events[index].data.fd);
                }else{
                    std::string str_result = "";
                    to_lower(str_message, str_result);   
                    send(ep_events[index].data.fd, str_result.c_str(), str_result.size(), 0);
                }
            }
        }
    }
    close(serv_sock);
    return 0;
}
相关推荐
Lbs_gemini06031 小时前
C++研发笔记14——C语言程序设计初阶学习笔记12
c语言·开发语言·c++·笔记·学习
MC何失眠1 小时前
vulnhub靶场【哈利波特】三部曲之Fawkes
网络·python·学习·网络安全
小袁顶风作案1 小时前
Ubuntu桥接模式设置静态IP
网络·tcp/ip·桥接模式
闲人-闲人2 小时前
CIA安全属性简介
网络·安全
我的老子姓彭3 小时前
C++学习笔记
c++·笔记·学习
hefaxiang4 小时前
【C++】数组
开发语言·c++
哎呦,帅小伙哦4 小时前
C++ 异步编程的利器std::future和std::promise
开发语言·c++
新兴AI民工5 小时前
C++中的操作系统级信号处理——signal与sigaction
c++·信号处理·signal·sigint·sigaction·操作系统信号处理
搬砖的果果5 小时前
HTTP代理有那些常见的安全协议?
服务器·python·网络协议·tcp/ip
Looper03315 小时前
【Shell 脚本实现 HTTP 请求的接收、解析、处理逻辑】
网络·网络协议·http