网络编程总结

使用read读取非阻塞socket

read()函数可能有三种返回值:

  • -1:出错或者无数据可读。
  • 0:对端关闭socket。
  • >0:正常读取到了数据。
#define REDIS_IOBUF_LEN         1024

void readQueryFromClient(int fd)
{
    char buf[REDIS_IOBUF_LEN];
    int nread;

    nread = read(fd, buf, REDIS_IOBUF_LEN);
    
    if (nread == -1)
    {
    // 处理EAGAIN(因为fd为非阻塞的)
        if (errno == EAGAIN)
        {
            nread = 0;
        }
    // read出错
    else
    {
            // TODO:断开连接
            return;
        }
    }
    // 客户端断开连接
    else if (nread == 0)
    {
        // TODO:断开连接
        return;
    }
    
    if (nread)
    {
        // TODO:处理buf
    }
    // 此处的else不能省略,因为当read()返回-1,且errno为EAGAIN时,表示此时无数据可读,会将nread置0,就会走else分支
    else
    {
        return;
    }
}

疑问:需要考虑read()函数被信号打断,从而返回-1的情况吗?在Redis源码中,没有考虑这一点。

使用while循环处理系统调用可能被信号中断的情况

// 版本1
int anetGenericAccept(char *err, int s, struct sockaddr *sa, socklen_t *len)
{
    int fd;
    // 使用while循环的目的是为了处理EINTR错误
    while(1)
    {
        fd = accept(s, sa, len);
        if (fd == -1)
        {
            if (errno == EINTR)
            {
              continue;
            }  
            else
            {
                anetSetError(err, "accept: %s", strerror(errno));
                return ANET_ERR;
            }
        }
        break;
    }
    return fd;
}
// 版本2
int anetGenericAccept(char *err, int s, struct sockaddr *sa, socklen_t *len)
{
  int fd;
  do
  {
    fd = accept(s, sa, len);
  } while(-1 == fd && EINTR == errno);

  if (-1 == fd)
  {
    anetSetError(err, "accept: %s", strerror(errno));
    return ANET_ERR;
  }

  return fd;
}

可变参数的使用

anetSetError(err, "accept: %s", strerror(errno));

static void anetSetError(char *err, const char *fmt, ...)
{
    va_list ap;

    if (!err)
    {
      return;
    }
    va_start(ap, fmt);
    vsnprintf(err, ANET_ERR_LEN, fmt, ap); // 其中,fmt既可以是格式化字符串(如"%s"),也可以是普通字符串(如"error"),或者两者结合(如"error:%s")
    va_end(ap);
}

检查线程是否存在

int ret = pthread_kill(tid, 0);
if (0 == ret)
{
  // 线程存在
}
else if (ESRCH == ret)
{
  // 线程不存在
}

解析PASV模式下的ftp服务器的IP和端口号

const char *passive_param = "(127,0,0,1,12,34)";

const char *p1 = strchr(passive_param, '(');
if (NULL == p1)
{
    // TODO
}
const char *p2 = strchr(passive_param, ')');
if (NULL == p2)
{
    // TODO
}
if (p1 >= p2)
{
    // TODO
}

int ip1, ip2, ip3, ip4;
int port1, port2;
sscanf(p1 + 1, "%d,%d,%d,%d,%d,%d", &ip1, &ip2, &ip3, &ip4, &port1, &port2);
char ip[16] = {0};
snprintf(ip, sizeof(ip), "%d.%d.%d.%d", ip1, ip2, ip3, ip4);
unsigned short port;
port = (port1 << 8) + port2; // 注意:port1<<8一定要加括号

通过inet_aton函数判断IP地址是域名还是点分十进制格式

if (inet_aton(addr, &sa.sin_addr) == 0) // 如果addr不是有效的IP地址,则inet_aton()会返回0
    {
        struct hostent *he;

        he = gethostbyname(addr);
        if (he == NULL)
        {
            __redisSetError(c,REDIS_ERR_OTHER,
                sdscatprintf(sdsempty(),"Can't resolve: %s",addr));
            close(s);
            return REDIS_ERR;
        }
        memcpy(&sa.sin_addr, he->h_addr, sizeof(struct in_addr));
    }

write函数的返回值不等于待写入的字节数

write()函数的返回值不等于待写入的字节数时,并不表示write()函数出错了。
例如:

  • 在Redis中:
int redisBufferWrite(redisContext *c, int *done) {
    int nwritten;
    if (sdslen(c->obuf) > 0)
    {
        nwritten = write(c->fd,c->obuf,sdslen(c->obuf));
        if (nwritten == -1)
        {
            if (errno == EAGAIN)
            {
                /* Try again later */
            }
            else
            {
                __redisSetError(c,REDIS_ERR_IO,NULL);
                return REDIS_ERR;
            }
        }
        else if (nwritten > 0) // 注意:当nwritten不等于sdslen(c->obuf)时,并不表示write()出错
        {
            if (nwritten == (signed)sdslen(c->obuf))
            {
                sdsfree(c->obuf);
                c->obuf = sdsempty();
            }
            else
            {
                c->obuf = sdsrange(c->obuf,nwritten,-1);
            }
        }
    }
    if (done != NULL)
    {
        *done = (sdslen(c->obuf) == 0);
    }
    return REDIS_OK;
}
  • 在muduo中:
void TcpConnection::sendInLoop(const void* data, size_t len)
{
  loop_->assertInLoopThread();
  ssize_t nwrote = 0;
  size_t remaining = len;
  bool faultError = false;
  if (state_ == kDisconnected)
  {
    LOG_WARN << "disconnected, give up writing";
    return;
  }
  // if no thing in output queue, try writing directly
  if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0)
  {
    nwrote = sockets::write(channel_->fd(), data, len);
    if (nwrote >= 0)
    {
      remaining = len - nwrote;
      if (remaining == 0 && writeCompleteCallback_)
      {
        loop_->queueInLoop(std::bind(writeCompleteCallback_, shared_from_this()));
      }
    }
    else // nwrote < 0
    {
      nwrote = 0;
      if (errno != EWOULDBLOCK)
      {
        LOG_SYSERR << "TcpConnection::sendInLoop";
        if (errno == EPIPE || errno == ECONNRESET) // FIXME: any others?
        {
          faultError = true;
        }
      }
    }
  }

  assert(remaining <= len);
  if (!faultError && remaining > 0)
  {
    size_t oldLen = outputBuffer_.readableBytes();
    if (oldLen + remaining >= highWaterMark_
        && oldLen < highWaterMark_
        && highWaterMarkCallback_)
    {
      loop_->queueInLoop(std::bind(highWaterMarkCallback_, shared_from_this(), oldLen + remaining));
    }
    outputBuffer_.append(static_cast<const char*>(data)+nwrote, remaining);
    if (!channel_->isWriting())
    {
      channel_->enableWriting();
    }
  }
}

对阻塞和非阻塞文件描述符的读或者写封装在一个函数中

即,在函数中要考虑read()或者write()返回-1的情况,并不代表函数出错了,也可能是资源暂不可用,需要判断errno进一步判断。
比如Redis客户端的读与写:

int redisBufferRead(redisContext *c)
{
    char buf[2048];
    int nread = read(c->fd,buf,sizeof(buf));
    if (nread == -1)
    {
        // 当fd为非阻塞,且fd暂不可读时,read()函数会返回-1,且errno等于EAGAIN
        if (errno == EAGAIN)
        {
            /* Try again later */
        }
        else
        {
            __redisSetError(c,REDIS_ERR_IO,NULL);
            return REDIS_ERR;
        }
    }
    else if (nread == 0)
    {
        __redisSetError(c,REDIS_ERR_EOF,
            sdsnew("Server closed the connection"));
        return REDIS_ERR;
    }
    else
    {
        __redisCreateReplyReader(c);
        redisReplyReaderFeed(c->reader,buf,nread);
    }
    return REDIS_OK;
}
int redisBufferWrite(redisContext *c, int *done)
{
    int nwritten;
    if (sdslen(c->obuf) > 0)
    {
        nwritten = write(c->fd,c->obuf,sdslen(c->obuf));
        if (nwritten == -1)
        {
            // 当fd为非阻塞时,会出现write返回-1,且errno等于EAGAIN的情况,即fd暂时不可写时。而在阻塞模式下,当fd暂时不可写时,会直接阻塞在write调用处
            if (errno == EAGAIN)
            {
                /* Try again later */
            }
            else
            {
                __redisSetError(c,REDIS_ERR_IO,NULL);
                return REDIS_ERR;
            }
        }
        else if (nwritten > 0) // 注意:当nwritten不等于sdslen(c->obuf)时,并不表示write()出错
        {
            // 写缓冲区中的数据全部写完
            if (nwritten == (signed)sdslen(c->obuf))
            {
                sdsfree(c->obuf); // 释放SDS
                c->obuf = sdsempty(); // 新建一个空SDS
            }
            // 写缓冲区中的数据未全部写完
            else
            {
                c->obuf = sdsrange(c->obuf,nwritten,-1); // 保留SDS中从nwritten到-1区间的数据,下次可写时再写
            }
        }
    }

    // 如果写缓冲区中的数据全部写完,则将*done置为1,表示完成
    if (done != NULL)
    {
        *done = (sdslen(c->obuf) == 0);
    }
    return REDIS_OK;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353