网络编程
简单的socket使用
服务端:
1、创建socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0); if (listenfd == -1) { std::cout << "create listen socket error." << std::endl; return -1; }
2、绑定ip和端口
struct sockaddr_in bindaddr; bindaddr.sin_family = AF_INET; bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); bindaddr.sin_port = htons(3000); if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1) { std::cout << "bind listen socket error." << std::endl; close(listenfd); return -1; }
3、监听端口
if (listen(listenfd, SOMAXCONN) == -1) { std::cout << "listen error." << std::endl; close(listenfd); return -1; }
4、接收连接
struct sockaddr_in clientaddr; socklen_t clientaddrlen = sizeof(clientaddr); int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen); if (clientfd != -1) { }
5、收发数据
char recvBuf[32] = {0}; int ret = recv(clientfd, recvBuf, 32, 0); if (ret > 0) { std::cout << "recv data from client, data: " << recvBuf << std::endl; ret = send(clientfd, recvBuf, strlen(recvBuf), 0); if (ret != strlen(recvBuf)) std::cout << "send data error." << std::endl; else std::cout << "send data to client successfully, data: " << recvBuf << std::endl; }
6、关闭和客户端的连接
close(clientfd);
7、关闭socket
close(listenfd);
客户端:
1、创建socket
int clientfd = socket(AF_INET, SOCK_STREAM, 0); if (clientfd == -1) { std::cout << "create client socket error." << std::endl; return -1; }
2、连接服务器
struct sockaddr_in serveraddr; serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); serveraddr.sin_port = htons(SERVER_PORT); if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) { std::cout << "connect socket error." << std::endl; close(clientfd); return -1; }
3、收发数据
int ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0); if (ret != strlen(SEND_DATA)) { std::cout << "send data error." << std::endl; close(clientfd); return -1; } std::cout << "send data successfully, data: " << SEND_DATA << std::endl; char recvBuf[32] = {0}; ret = recv(clientfd, recvBuf, 32, 0); if (ret > 0) { std::cout << "recv data successfully, data: " << recvBuf << std::endl; }
4、关闭连接
close(clientfd);
select的使用
上面的socket是最简单的使用方式,实际的服务器开发,我们需要在一个进程中处理成百上千个客户端的同时连接,上面的socket就不能适用了,select是一个简单的io复用技术,就是可以在一个进程的一个线程里面同时监听多个客户端的连接,并处理对应的数据收发。
int ret = select(maxfd + 1, &readset, NULL, NULL, &tm); //第一个参数是本地需要监听的socket的最大值,第二个参数是读事件集合,第三个参数是写事件集合,第四个参数是异常处理事件集合,第五个参数是超时时间 fd_set readset; //需要监听的事件集合定义 FD_ZERO(&readset); //集合清零 FD_SET(listenfd, &readset); //将某个事件加入集合 FD_ISSET(listenfd, &readset) //当触发某个事件的时候判断一下是不是这个事件。如果是本地监听socket,需要处理客户端的连接,调用accept,如果不是本地监听的socket,就是客户端的某个连接,需要处理对应的数据的收发
非阻塞状态的socket
除了采用select的io复用方式提高服务器的性能外,socket如果是阻塞方式的话,当线程阻塞住就不能做其他事情,会浪费资源,所以高性能服务器的socket一般会把socket设置成非阻塞状态。
//连接成功以后,我们再将clientfd设置为非阻塞模式, //不能在创建时就设置,这样会影响到connect函数的行为 int oldSocketFlag = fcntl(clientfd, F_GETFL, 0); int newSocketFlag = oldSocketFlag | O_NONBLOCK; if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1) { close(clientfd); std::cout << "set socket to nonblock error." << std::endl; return -1; }
connect、accept、send、recv在非阻塞状态下都会发送改变。
send、recv在阻塞状态下,如果内核数据满了,send会将线程阻塞住,如果内核没有数据recv会将线程阻塞住。非阻塞状态下不会阻塞线程,看返回值,>0表示发送成功或接收成功,=0表示对端关闭连接,<0(-1)出错。如果尝试发送字节为0的数据,对端是不会有任何反应。返回-1的情况有,tcp窗口太小,信号中断或者出错。
connect在阻塞状态下,如果连接不上会阻塞线程,如果由于ip较远,连接较慢,会阻塞几秒钟。非阻塞状态下不会阻塞线程,当返回0表示连接成功,否则暂时连接不上,需要看错误码,EINPROGRESS表示正在连接,EINTR表示连接中断正在重试,其他情况表示出错。没有连接成功,错误码不是出错的话,可以用select监听,但是即使可写了,也要用getsocket检测一下socket是否出错再进行发数据。
从缓存区得到有多少数据可读
//socket 可读时获取当前接收缓冲区中的字节数目 ulong bytesToRecv = 0; if (ioctl(fds[i].fd, FIONREAD, &bytesToRecv) == 0) { std::cout << "bytesToRecv: " << bytesToRecv << std::endl; }
poll的使用
除了select可以进行io复用外,poll也可以,使用方式:
std::vector<pollfd> fds; pollfd listen_fd_info; listen_fd_info.fd = listenfd; listen_fd_info.events = POLLIN; //设置监听对象是读 listen_fd_info.revents = 0; fds.push_back(listen_fd_info); n = poll(&fds[0], fds.size(), 1000); //第一个参数是一个vector,里面都是pollfd结构体对象,第二个参数是vector大小,第三个参数是超时时间。 if (n < 0) { //被信号中断 if (errno == EINTR) continue; //出错,退出 break; } else if (n == 0) { //超时,继续 continue; } //正常处理业务
和select相比,优点:
1、不用计算最大文件描述符+1的大小;
2、和select相比,处理大数量的文件描述符更快;
3、poll没有最大连接数的限制;
4、在调用poll的时候参数设置一次就可以了。
poll有缺点:
1、在调用poll的时候,不论有没有意义,大量的fd的数组在用户态和内核态地址空间被整体复制;
2、与select函数一样,poll函数返回后,需要遍历fd集合来获取就绪的fd,性能会下降;
3、如果同时连接大量的客户端,某时刻可能只有很少的就绪状态,性能不太好看。
epoll的使用
select和poll都是有缺点的,主要是在有大量连接的时候性能不好看,epoll可以解决该问题,用法:
int epollfd = epoll_create(1); //创建一个epoll文件描述符 epoll_event listen_fd_event; //定义一个监听事件 listen_fd_event.data.fd = listenfd; //事件绑定一个监听的文件描述符 listen_fd_event.events = EPOLLIN; //监听读事件,EPOLLOUT 写事件, EPOLLET边缘模式,默认是水平模式,EPOLLONESHOT只触发一次 epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &listen_fd_event) == -1 //将需要监听的文件描述符和事件添加到epoll文件描述符上 epoll_event epoll_events[1024]; //事件接收容器 n = epoll_wait(epollfd, epoll_events, 1024, 1000); //等待io事件,第一个参数是epoll文件描述符,第二个参数是事件接收容器,第三个参数是容器大小,第四个参数是超时时间。 if (n < 0) { if (errno == EINTR) continue; break; } else if (n == 0) { continue; } for (size_t i = 0; i < n; ++i) { if (epoll_events[i].events & EPOLLIN) { //相关业务处理 } }
网络字节序
字节序有大端和小端两种:
小端是整数高位存储在内存高地址,整数低位存储在内存低地址;
大端是整数低位存储在内存低地址,整数地位存储在内存高地址;
网络字节序采用大端方式。
粘包问题
如何解决粘包、丢包和乱序问题。其实用tcp就不存在丢包和乱序问题,如果是udp需要自己写有序和可靠的传输机制。粘包是指发送方发了几个包,对方收到后可能收到半个包,或者几个包拼在一起的情况,因为tcp是数据流的形式传输,解决方式:
1、固定包的长度;
2、以指定的字符串作为包的结尾标志;
3、包头+包尾形式