socket流程

几个关于socket库的结论。

  • 基于同一个socket上的操作必须放在同一个线程,包括创建、连接、读、写、关闭。——如何快速得出这结论?TCPSocketWin有个thread_checker_成员,执行操作时会要求DCHECK_CALLED_ON_VALID_THREAD(thread_checker_)。
  • 只支持异步方式,当操作返回值是ERR_IO_PENDING,表示此次是异步返回。
  • 如果Connect/Read/Write返回ERR_IO_PENDING,在没触发下次连接完成/可读/可写事件前,禁止再次调用Connect/Read/Write。——如何快速得出这结论?像TCPSocketWin::Read首先会要求core_->read_iobuffer_必须是nullptr,一次触发了ERR_IO_PENDING的异步读会把read_iobuffer_设为有效值,此个异步读的后半部完成后,read_iobuffer_恢复到nullptr。

一、异步(Async/Unblock/Overlapped)

以Windows为例,描述Chromium如何实现异步。

图3 Windows下的异步实现
  1. app在socket线程调用socket库的api:Read。buf、len是和收到数据后要放到缓冲区相关的两个参数。callback是发生异步时,recv读出数据后要调用的函数,它会保存到变量read_callback_。如果此次是同步读出,不会调用这函数。
  2. Read调用内部函数ReadIfReady,ReadIfReady也须要一个回调函数作为参数,值是RetryRead,它会保存到变量read_if_ready_callback_。
  3. ReadIfReady调用系统api:recv。在Windows,recv返回SOCKET_ERROR、并且WSAGetLastError返回WSAEWOULDBLOCK,表示发生异步了。接下调用core_->WatchForRead()。
  4. WatchForRead会调用ObjectWatcher::StartWatchingInternal。后者执行三个操作,1)调用SequencedTaskRunnerHandle::Get()得到本线程的TaskRunner,赋给task_runner_,后面要用它把运行环境从ntdll.dll线程转到socket线程。2)把ObjectWatcher::Signal赋给callback_。3)调用系统api:RegisterWaitForSingleObject,等待操作系统完成此次异步读。对RegisterWaitForSingleObject中参数,回调设的是DoneWaiting。等待时间设INFINITE,即无限等待,app想中止等待,只能通过关闭这个socket。——至此TCPSocketWin::Read结束了,返回值ERR_IO_PENDING。
  5. 将来某个时刻,系统检测到这个socket上的异步读完成,调用ObjectWatcher::DoneWaiting。运行DoneWaiting是系统内部线程(ntdll.dll),为便于后续处理,立即向task_runner_投递任务callback_,即ObjectWatcher::Signal。
  6. 上面已说过,task_runner_是socket线程的TaskRunner,于是转为在socket线程执行ObjectWatcher::Signal,它基本等同调用ReadDelegate::OnObjectSignaled。
  7. OnObjectSignaled调用TCPSocketWin::DidSignalRead,后者调用read_if_ready_callback_,即RetryRead。RetryRead执行两个操作,1)调用系统函数recv读出数据,2)调用app在Read时传入的回调read_callback_。——至此操作转入app在Read时传入的回调,运行环境是socket线程。

二、让代码同时支持同步和异步

不论是Connect,还是Read、Write,它们都有可能异步(ERR_IO_PENDING)或同步返回(非ERR_IO_PENDING),代码要同时支持这两种情况。

<chromium>/net/socket/transport_client_socket_pool.cc
int TransportConnectJob::DoLoop(int result) {
  DCHECK_NE(next_state_, STATE_NONE);

  int rv = result;
  do {
    State state = next_state_;
    next_state_ = STATE_NONE;
    switch (state) {
      case STATE_RESOLVE_HOST:
        DCHECK_EQ(OK, rv);
        rv = DoResolveHost();
        break;
      case STATE_RESOLVE_HOST_COMPLETE:
        rv = DoResolveHostComplete(rv);
        break;
      case STATE_TRANSPORT_CONNECT:
        DCHECK_EQ(OK, rv);
        rv = DoTransportConnect();
        break;
      case STATE_TRANSPORT_CONNECT_COMPLETE:
        rv = DoTransportConnectComplete(rv);
        break;
      default:
        NOTREACHED();
        rv = ERR_FAILED;
        break;
    }
  } while (rv != ERR_IO_PENDING && next_state_ != STATE_NONE);

  return rv;
}

TransportConnectJob用一个状态机描述连接过程,DoTransportConnect负责连接向一个ip地址,以它为例看代码是如何同时支持同步和异步。

void TransportConnectJob::OnIOComplete(int result) {
	result = DoLoop(result);
	if (result != ERR_IO_PENDING) {
		...
	}
}

int TransportConnectJob::DoTransportConnect() {
	next_state_ = STATE_TRANSPORT_CONNECT_COMPLETE;
	...
	transport_socket_ = client_socket_factory_->CreateTransportClientSocket(addresses_, ...);
	...
	int rv = transport_socket_->Connect(base::Bind(&TransportConnectJob::OnIOComplete, base::Unretained(this)));
	return rv;
}

DoTransportConnect调用Connect,当Connect是同步返回时,即0。回到上层TransportConnectJob::DoLoop,满足while继续循环条件“rv != ERR_IO_PENDING && next_state_ != STATE_NONE”,于是进入下一状态STATE_TRANSPORT_CONNECT_COMPLETE。

当Connect是异步返回时,即ERR_IO_PENDING。回到上层TransportConnectJob::DoLoop,不再满足while继续循环条件,DoLoop以ERR_IO_PENDING退出。将来某个时刻,系统检测到这个socket上的异步连接完成,Connect时设的回调TransportConnectJob::OnIOComplete被调用,后者调用DoLoop,于是也和同步时一样,进入下一状态STATE_TRANSPORT_CONNECT_COMPLETE。

如何让代码同时支持同步和异步?为方便定义几个术语。

  • 行为例程。执行该状态下特定行为的操作叫行为例程,对应DoTransportConnect。
  • 完成例程。异步完成后调用的操作叫完成例程,对应TransportConnectJob::OnIOComplete。
  • 网络操作。行为例程中,可能产生同步、异步结果的操作叫网络操作。不是所有这类操作都和网络有关,用这名称只是为代指。对应transport_socket_->Connect。
  • 后绪操作。在行为例程中,执行网络操作后如果还有操作,那些称为后绪操作。后绪操作也可能出现在完成例程。DoTransportConnect中没有后绪操作。
  • 结束状态。定义的状态往往是成对出现,像STATE_TRANSPORT_CONNECT和STATE_TRANSPORT_CONNECT_COMPLETE,后面带有_COMPLETE后缀的称为结束状态。以下会用xxx_COMPLETE表示结束状态。

以下是建议。

  • 用一个状态机,DoLoop处理这个状态机。
  • DoLoop中的while循环使用条件:rv != ERR_IO_PENDING && next_state_ != STATE_NONE。switch前执行“next_state_ = STATE_NONE”,原因见后面的“结束状态”。
  • next_state_。初始值是STATE_NONE,一旦要开始DoLoop,必须设置为新值。
  • 两种result。第一种是表示网络操作的执行结果,它不总是类似net::OK,ERR_IO_PENDING表示网络状态的值,像Read成功时会是读到的字节数。它会出现在三个地方,1)完成例程的输入参数。2)后绪操作输入参数。第二种是统一表示状态的值。它会出现在三个地方。1)后绪操作的返回结果。2)DoLoop的输入参数。3)行为例程的返回值。两种result很大部分值是一致的,转换这个操作在后绪操作中执行。
  • 行为例程。细分为三个任务。1)设置next_state_到新值。即不论内中操作是同步还是异步,next_state_都要进入新状态。2)执行网络操作,当中要挂接完成列程。3)是同步返回时,有“后绪操作”的要执行。是异步返回时,表示后续任务得触发异步完成才能处理了,不执行“后绪操作”,改放在完成例程去执行。
  • 完成例程。细分为三个任务。1)判断输入参数result,理论上不可能是ERR_IO_PENDING,如果是啥都不做,不是ERR_IO_PENDIN时执行后面操作。2)执行“后绪操作”。3)调用DoLoop,参数就是result。
  • 后绪操作。后绪操作会有一个输入参数result,表示网络操作的结果。1)判断result,若是错误要采取相应操作,像断开连接,并向上返回。2)具体动作。像read到数据了,处理这些数据。3)转换返回值。经过后绪操作后,返回值将统一变成表示“状态”的值,像read/write表示收到字节数的要转为OK。
  • 结束状态。1)在分工上,xxx执行带网络操作的行为例程,xxx_COMPLETE执行处理xxx的执行结果。结束状态中往往没有网络操作,但会有一个表示xxx执行结果的输入参数result,在read/write时,result不是表示字节数,而是用OK表示了。2)结束状态时行为例程的操作:首先判断result是否是OK,不是OK就返回。然后修改next_state_到下一状态。3)增加结束状态的一个目的是为处理xxx时发生的错误。xxx的行为例程在处理网络操作前,已经把next_state设为不是STATE_NONE,一旦网络操作发生错误,即此时result不是ERR_IO_PENDING、且next_stte不是STATE_NONE,导致退不出DoLoop中的while循环。有了结束状态后,result会传给结束状态时的行为例程,它发现result不是OK,就向DoLoop返回,且此时next_state是STATE_NONE,于是退出了DoLoop的while循环。

全部评论: 0

    写评论: