网络编程
简单的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)
{
//相关业务处理
}
}
网络字节序
字节序有大端和小端两种:
小端是整数高位存储在内存高地址,整数低位存储在内存低地址;
大端是整数低位存储在内存低地址,整数地位存储在内存高地址;
网络字节序采用大端方式。










