网络编程学习

网络编程

简单的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、包头+包尾形式

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值