Socket编程本身已经是讲烂了的一个过程,无非就是一个bind,connect,accept的过程,牵扯到IO操作之后会有一点点复杂,前几天写了一个简单基于epoll的并发服务器程序,这里将所用到的知识点总结一下。
创建socket: int socket(int family, int type, int protocal) , 常见创建TCP socket的调用如下: int sock = socket(AF_INET, SOCK_STREAM, 0); 这个点比较简单,没什么好讲的,就是注意有时候在网上会遇到PF_INET这个标示,这是是由于历史遗留问题,在<sys/socket.h> 中,AF_xx与PF_xx的定义是相同的,所以PF_INET 和 AF_INET是等效的,自己写代码的时候最好使用AF开头的标识。
绑定:int bind(int socket, const struct sockaddr* myaddr, socklen_t addrlen); 这一步比较烦,因为牵扯到地址的数据结构,地址结构有两种表达方式,一种是IPv4的地址结构,在<netinet/in.h> 中定义,一种是通用套接字地址结构,在<sys/socket.h>中定义,这两者是等价的,前者好赋值一些,后者是bind中定义的结构,因此我们一般定义sockaddr_in结构进行地址及端口的赋值,然后采用强制类型转换成sockaddr类型在bind函数中操作。同时需要注意的是,由于网络协议在处理多字节程序时,多采用大端字节序,而主机多采用小端字节序,所以在对地址赋值的时候需要进行字节之间的转换,采用htons 或者 htonl 函数处理,定义在<betiner/in.h> 中,在真实的使用中,我们不用关心本机采用的是大端还是小端,在采用大端字节的本机系统中,这两个函数会定义为空函数,所以只要调用这两个函数就ok了。如果想绑定本机IP地址,直接采用通配地址INADDR_ANY即可,它告诉内核自己去选择IP地址。
struct in_addr {
in_addr_t s_addr;
};
struct sockaddr_in{
uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
struct sockaddr {
uint8_t sa_len;
sa_family_t sa_family;
char sa_data[14];
};
监听:int listen( int sockfd, int backlog),这个函数将socket由主动套接口变成一个被动套接口,指示内核应该接受来自该套接口的请求。其中第二个参数规定了相应套接口排队的最大排队数,可以理解城同时处理的请求的个数,一般每天请求在几百个以内的服务器将backlog设成5即可。
int accept(int sockfd, struct sockaddr* cliaddr, scoklen_t *addrlen); 该函数用于从已完成连接队列中返回下一个已完成链接。如果已完成队列为空怎么办?这里就要讲一讲阻塞socket和非阻塞socket的区别了。
所谓阻塞的socket,就是基于此socket的数据接收发送及connect,accept等操作都是阻塞的,即如果没有结果就一直等待(进程投入睡眠),直到返回结果,同理,非阻塞的socket就是基于此socket的之前操作都是非阻塞的,即如果没有结果就返回一个错误(而不是将进程投入睡眠)。通过socket ()创建的socket默认都是阻塞的,如果想将socket从阻塞变为非阻塞,可以采用 fcntl 函数,包含在<fcntl.h> 头文件中,此函数可执行各种描述字控制操作,函数原型是:int fcntl ( int fd, int cmd, ...); 使用fcntl函数开启非阻塞socket的典型代码如下,至于非阻塞的socket的巨大作用,在后面用到epoll的时候就可以看到非阻塞socket的作用了。
int flags;
if ((flag = fcntl(fd, F_GETFL, 0)) < 0)
error ("F_GETFL error");
flags |= O_NONBLOCK;
if ( fcntl(fd, F_SETFL, flags) < 0)
error ("F_SETFL error");
回到上文,对于阻塞的accept,当已完成队列为空,则将当前进程投入到睡眠中,当有新的连接请求到来时再由内核唤醒然后返回;而对于非阻塞的accept,当已完成队列为空时,则直接返回一个EWOULDBLOCK的错误。
对于accpet函数,会牵扯到两个socket,区分这两个socket很关键,函数的第一个参数 sockfd 是监听套接字,即调用accept 之前 绑定( bind ) 并监听 ( listen ) 的那个socket,然后如果accept 成功的话会返回一个新的socket,返回的这个socket代表了新收到并建立的连接。第二个参数 cliaddr 由内核填充了新收到连接的对端进程的地址。
至此连接已经建立成功了,下一步可以进行数据交互了,数据IO常用以下两个函数
#include <sys/socket.h>
ssize_t recv(int sockfd, void* buf, ssize_t nbytes, int flags);
ssize_t send(int sockfd, const void* buf, ssize_t nbytes, int flags);
这两个函数类似 read 和 write,只是最后多了一个参数 flags,可以置0或进行其它操作。
调用recv时,如果成功,则返回读到的字节数,如果返回零,则说明对方的socket正常关闭,如果小于零出错,而对于非阻塞的socket来说,如果返回的错误类型是 EAGAIN或EWOULDBLOCK,则说明暂时没有数据请稍后再试。因此一个接收数据的函数可以写成下面这样:
bool readCmd() { int ret; while(1) { int max = recvBuffer.getLeft(); if (max < MAX_BUFSIZE) { recvBuffer.resize(recvBuffer.size()+MAX_BUFSIZE); max += MAX_BUFSIZE; } if((ret = recv(fd, (void*)recvBuffer.getCurAddr(), max, 0)) > 0) { std::cout << "Some data read "<<std::endl; recvBuffer.push(ret); } else if(ret == 0) { std::cout << "Socket "<<fd<<" is closed on the other hand"<<std::endl; return false; } else if(ret < 0) { if((errno != EAGAIN) && (errno != EWOULDBLOCK )) { std::cout << "Socket "<<fd<<" is sth wrong for failed recv data"<<std::endl; return false; } return true; } } }
使用while反复调用recv是为了确保所有数据都被接收。
与recv相比,send函数比较简单,成功返回已发送的字符数,若出错则返回-1,若是非阻塞的socket,还需要处理EAGAIN 和 EWOULDBLOCK 这两种错误,代码如下
bool sendCmd() { int ret = send(fd, sendBuffer.getBeginAddr(), all, 0); if(ret < 0) { if((errno != EAGAIN) && (errno != EWOULDBLOCK)) { std::cout<<"Socket " <<fd << " send Cmd error" << std::endl; return false; } } else { std::cout << "Some data send" << std::endl; } return true; }
OK,这样的话写个简单的网络编程应该没有问题了,下一章总结一个利用epoll实现并发的方法。