Socket 学习总结

接下来要总结的是TCP socket通信

 

socket 分为阻塞式和非阻塞式的, 下面一个个介绍。

 

socket默认是阻塞式的, 你需要手动设置 非阻塞属性,才能达到非阻塞。  

windows 下面 使用如下函数设置非阻塞

ioctlsocket(SockClient,FIONBIO,&flag)    // flag == 0代表阻塞   flag == 1代表的是非阻塞

linux使用如下函数设置非阻塞

fcntl(m_sock, F_SETFL, O_NONBLOCK);

 

1.阻塞式socket  

阻塞模式下,在I/O操作完成前,执行的操作函数一直等候而不会立即返回,该函数所在的线程会阻塞在这里。相反,在非阻塞模式下,套接字函数会立即返回,而不管I/O是否完成,该函数所在的线程会继续运行。

在阻塞模式的套接字上,调用任何一个Sockets API都会耗费不确定的等待时间。

 

当调用 connect函数时,当网络不好的时候,connect函数返回有可能要经历75秒到几分钟的时间,这个时间是我们无法控制的,对于任何应用来说,这么做都是太不友好的,常用的处理方式是使用非阻塞模式 加上select, 在select模型中加上超时连接时间,而这个超时时间是可以自己来设置的。

 

当调用recv()函数时,系统首先查是否有准备好的数据。如果数据没有准备好,那么系统就处于等待状态。当数据准备好后,将数据从系统缓冲区复制到用户空间,然后该函数返回。在套接应用程序中,当调用recv()函数时,未必用户空间就已经存在数据,那么此时recv()函数就会处于等待状态。

 

当调用send函数的时候,如果发生的数据大于底层缓冲数据的时候, send 会阻塞一段时间,  如果发送的数据小于底层缓冲区的数据, send会立即返回。

 

另外要说一下,  send 和recv 不是发送数据给服务器端, 而是把要发送的数据,拷贝的到底层缓冲区, 有底层的协议去发送数据,

recv也是一样的,是从底层缓冲区中 拷贝数据到用户的缓冲区中。 具体的发送和接收都是有底层协议来做的。

阻塞式socket 管理起来比较简单,没数据的时候阻塞在那里,有数据的时候就接收,然后处理。常用的处理方式是,一个阻塞式socket

建立一个线程来管理,有几个阻塞式socket,就建立几个线程来管理,这样管理起来时比较方便,但是太浪费资源。

 

2.非阻塞socket


非阻塞socket,也就是调用一些函数时,会立即返回,后面通过判断一些错误码来判断这些函数是否调用错误。如下这些错误码,可以认为是非错误的


其他的错误码都可以标记为错误,根据错误来进行相应的处理。

 Socket 学习总结_第1张图片

那非阻塞socket 又是如何知道何时连接成功,何时可接收数据呢?

 

有如下几个模型(select模型  poll模型, epoll模型), 下面主要介绍select模型:

select 函数监视的文件描述符分3类,分别是writefdsreadfds、和exceptfds。调用后select函数会阻塞,直到有描述副就绪(有数据可读、可写、或者有except),或者超时(timeout指定等待时间,那么就是超时时间为0,那么select就会立刻返回了,如果null那么超时就未被启用,会一直阻塞在监视文件描述符的地方),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。

select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。


如下我是在MFC实现的select代码:客户端部分代码:


Connect返回之后,根据错误码,来判断是否正在建立连接,如果正在建立连接的话,

就建立一个线程,通过这个线程来判断,是否连接成功(通过判断该socket是否可写

见下图》

Socket 学习总结_第2张图片


如果连接成功了,其实就可以不用检测可写集(我下面面的例子没去掉)。

只需检测可读集, 如果有可读消息, 就发送消息给主窗口,去recv消息。

见下图》

Socket 学习总结_第3张图片

Socket 学习总结_第4张图片


Socket 学习总结_第5张图片


我的这个例子,是从子线程收到连接成功的消息,可读的消息之后,然后发给主线程窗口来处理接收(recv)。其实这样做 ,是有如下问题的,

如果主线程还未处理recv消息,此时子线程又select发现又有可读消息,会再发一个可读消息给主线程窗口,所以又要调用一些 recv函数,如果第一个recv函数,把数据从底层缓冲区取完了,第二个recv就会返回 -1 ,所以要根据错误来判断。或者也可以通过另外一种方式来处理加入状态机:在发送可读消息之后,就把状态置为正在 Recving,接收数据之后,就把状态修改为ReceivedRecving状态时,子线程在循环时,子线程可以先不执行( 下面服务器端代码是按照这种方式来实现的);

 

 

Select 函数返回值的问题:

返回-1代表错误。  返回0 代表超时, 返回n > 0, 代表有n个socket在你设置的集合中。

可以根据n 做后续的处理操作。

 

最后要说的就是,子线程跟主线程之间的通信问题:  我上面的例子是,子线程通过发消息给主线程,让主线程来接收数据。(也就是发消息的方式来做的), 其实比较流行的是使用消息队列,主线程和子线程互斥访问这个消息队列, 子线程往队列里面放数据, 主线程从队列里面出去。

 

 

阻塞模式下服务器端 accept 函数也是阻塞函数,在非阻塞模式下,应该何时调用accept呢? 

 

单独给accept开一个线程同样使用select模型来检查当前的监听socket 是否可读,如果在可读集中就说明有连接过来。


Socket 学习总结_第6张图片


Socket 学习总结_第7张图片

Socket 学习总结_第8张图片


如果socket可读,说明有客户端链接,然后要创建一个发送和接收的socket,并单独开一个线程来处理当前socket的接收和发送。只要建立一个连接,都需要创建一个新的线程,来处理socket的发送和接收。


Socket 学习总结_第9张图片

Socket 学习总结_第10张图片


Socket 学习总结_第11张图片


Socket 学习总结_第12张图片




以上是我对socket的理解,有什么不对的, 请大家指正,共同学习进步。

 

多谢这些帖子,对我加深理解很有帮助

http://www.cnblogs.com/jianqiang2010/archive/2010/08/20/1804598.html

http://www.cppblog.com/artmouse/archive/2005/12/14/1762.aspx


你可能感兴趣的:(Socket)