基于epoll的socket编程(1)------ socket编程基础

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实现并发的方法。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值