TCP/IP网络编程-C++ (下) 一、基于Linux的进阶服务端 1、`shutdown()` 函数 - 优雅的断开连接 1.1、函数原型 1.2 参数解析 1.3 `shutdown()` 函数在client端应用示例代码: 2、IP地址和域名相互转换 2.1、`gethostbyname()` - 域名转换为IP 2.2 `gethostbyaddr()` - IP转换为域名 3、`select()`函数实现I/O复用服务器 3.1、`select()`函数介绍 3.2、I/O复用服务器代码示例: 4、高级I/O - `readv()` & `writev()` 函数 4.1、`writev()`函数介绍 4.2、`readv()`函数介绍 5、优与select的epoll 5.1、`epoll_create()` 函数 - 创建保存epoll文件描述符的空间 5.2、`epoll_ctl()` 函数- 向空间注册并注销文件描述符 5.3、`epoll_wait()` 函数 - 等待文件描述符发生变化 5.4、基于epoll的字符串转换服务端代码实现
一、基于Linux的进阶服务端
1、shutdown()
函数 - 优雅的断开连接
1.1、函数原型
int shutdown ( int sock, int howto) ;
1.2 参数解析
参数 参数解析 sock 需要断开连接的套接字描述符 howto 传递端口方式信息
howto可传递值 说明 SHUT_RD 断开输入流,套接字无法再接收数据 SHUT_WR 断开输出流,套接字无法再发送数据 SHUT_RDWR 断开输入流和输出流,套接字无法接受和发送数据
如果shutdown()第二个参数传递“SHUT_RDWR”参数,这样和close()函数有什么区别呢?区别在于使用shutdown()的“SHUT_RDWR”方式关闭,只是会关闭发送和接收数据的功能,并不会关闭套接字描述符,也就是说套接字资源并没有被释放。而close()函数不仅关闭了发送和接收数据的功能,同时关闭了套接字,释放了相关资源。
1.3 shutdown()
函数在client端应用示例代码:
const int BUFFER_SIZE = 128 ;
int main ( void ) {
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" ; } 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
# include <netdb.h>
struct hostent * gethostbyname ( const char * hostname) ;
struct hostent
{ char * h_name; char * * h_aliases; int h_addrtype; int h_length; char * * h_addr_list;
} ;
hostent结构体成员 说明 h_name 存放官方域名 h_aliases 存放除官方域名外的其它域名,可以通过这些域名访问同一主页 h_addrtype 保存IP的地址族信息,若是IPv4则保存AF_INET,若是IPv6则保存PF_INET6 h_length 保存IP地址长度 h_addr_list 以整数形式保存域名对应的IP地址
# 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 ;
}
2.2 gethostbyaddr()
- IP转换为域名
# include <netdb.h>
struct hostent * gethostbyaddr ( const char * addr, socklen_t len, int family) ;
参数 参数解析 addr 含有IP地址信息的in_addr结构体指针。为了同时传递IP地址信息外的其它信息,该变量声明为char*类型 len 地址信息的字节数,IPv4时为4,IPv6时为16 family 地址族信息
# 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 ;
}
3、select()
函数实现I/O复用服务器
3.1、select()
函数介绍
# 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) ;
参数 参数解析 maxfd 监视对象文件描述符数量 readset 将所有关注“是否存在待读取数据”的文件描述符注册到fd_set变量,并传递其地址值 writeset 将所有关注“是否可传输无阻塞数据”的文件描述符注册到fd_set变量,并传递其地址值 exceptset 将所有关注“是否发生异常”的文件描述符注册到fd_set变量,并传递其地址值 timeout 为防止select()函数陷入无限阻塞状态,所以传递timeout,到达时间后没有监视到变化也会返回 返回值 发生错误时返回-1,超时时返回0(就是达到了timeout时间还未监视到变化),关注的事件发生变化时返回发生变化的文件描述符数量
fd_set
类型说明: (1)、fd_set定义原型
typedef struct { long int fds_bits[ 1024 ] ;
} fd_set;
(2)、fd_set就一个long int
类型,大小为1024的数组,其下标就表示套接字的文件描述符,所有可以同时监视1024个套接字。每一位都初始化为0,如果某位值为1,则表示监视该套接字。如果下标为3的位值为1,表示监视文件描述符为3的套接字。 (3)、操作fd_set的宏
FD_ZERO ( fd_set* fdset) ;
FD_SET ( int fd, fd_set* fdset) ;
FD_CLR ( int fd, fd_set* fdset) ;
FD_ISSET ( int fd, fd_set* fdset) ;
timeval
参数说明: (1)、原型及成员说明:
struct timeval { long tv_sec; long tv_usec;
}
使用select()
函数时可以将多个文件描述符集中到一起监视,会关注如下变化: (1)、是否存在套接字接收数据 (2)、无需阻塞传输数据的套接字有哪些 (3)、哪些套接字发生了异常
第一步: 1、设置文件描述符:利用fd_set注册文件描述符 2、指定监视范围:范围就是套接字文件描述符最大值+1,也就是select()函数的第一个参数 3、设置超时:将超时时间填在timeout中。 第二步: 1、调用select()函数监听事件 第三步: 1、查看返回结果,获取变化的文件描述符进行相应处理
3.2、I/O复用服务器代码示例:
此示例基于以前的字符串转换的服务端修改的,客户端没有变化,可以使用之前的客户端来验证测试。
# 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()
函数介绍
# include <sys/uio.h>
ssize_t writev ( int filedes, const struct iovec * iov, int iovcnt) ;
参数 参数解析 filedes 套接字文件描述符 iov iovec结构体 数组 的地址,iovec结构体包含待发送的数据和大小 iovcnt iov参数的长度
struct iovec { void * iov_base; size_t iov_len;
}
# 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()
函数介绍
# include <sys/uio.h>
ssize_t readv ( int filedes, const struct iovec * iov, int iovcnt) ;
# 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文件描述符的空间
# include <sys/epoll.h>
int epoll_create ( int size) ;
调用epoll_create()
函数时创建的文件描述符保存空间称为“epoll例程”,通过参数size传递的值决定“epoll例程”的大小,但该值并不能决定“epoll例程”的大小,而是仅供操作系统参考。
5.2、epoll_ctl()
函数- 向空间注册并注销文件描述符
# include <sys/epoll.h>
int epoll_ctl ( int epfd, int op, int fd, struct epoll_event * event) ;
参数 参数解析 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 更改注册的文件描述的关注事件发生情况
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()
函数 - 等待文件描述符发生变化
# include <sys/epoll.h>
int epoll_wait ( int epfd, struct epoll_event * events, int maxevents, int timeout) ;
参数 参数解析 epfd 表示事件发生监视范围的epoll例程的文件描述符 events 保存发生事件的文件描述符集合的结构体地址,其所需要的缓冲要使用malloc()动态分配 maxevents 第二个参数可保存的最大事件数 timeout 以毫秒为单位的等待时间,传递-1时会一直等待直到事件发生
5.4、基于epoll的字符串转换服务端代码实现
此示例基于使用select()
服务端修改,客户端没有变化,使用之前的服务端即可测试
# 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 ;
}