TCP/IP网络编程-C++ (下)
- 一、基于Linux的进阶服务端
-
- [1、`shutdown()` 函数 - 优雅的断开连接](#1、
shutdown()
函数 - 优雅的断开连接) -
- 1.1、函数原型
- [1.2 参数解析](#1.2 参数解析)
- [1.3 `shutdown()` 函数在client端应用示例代码:](#1.3
shutdown()
函数在client端应用示例代码:)
- 2、IP地址和域名相互转换
-
- [2.1、`gethostbyname()` - 域名转换为IP](#2.1、
gethostbyname()
- 域名转换为IP) - [2.2 `gethostbyaddr()` - IP转换为域名](#2.2
gethostbyaddr()
- IP转换为域名)
- [2.1、`gethostbyname()` - 域名转换为IP](#2.1、
- 3、`select()`函数实现I/O复用服务器
- [4、高级I/O - `readv()` & `writev()` 函数](#4、高级I/O -
readv()
&writev()
函数) - 5、优与select的epoll
-
- [5.1、`epoll_create()` 函数 - 创建保存epoll文件描述符的空间](#5.1、
epoll_create()
函数 - 创建保存epoll文件描述符的空间) - [5.2、`epoll_ctl()` 函数- 向空间注册并注销文件描述符](#5.2、
epoll_ctl()
函数- 向空间注册并注销文件描述符) - [5.3、`epoll_wait()` 函数 - 等待文件描述符发生变化](#5.3、
epoll_wait()
函数 - 等待文件描述符发生变化) - 5.4、基于epoll的字符串转换服务端代码实现
- [5.1、`epoll_create()` 函数 - 创建保存epoll文件描述符的空间](#5.1、
- [1、`shutdown()` 函数 - 优雅的断开连接](#1、
一、基于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结构
cppstruct 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;
}