1. TCP/IP、UDP的基本概念
TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,他是一个工业标准的协议集,它是为广域网设计的。其中包含了很多其他的协议,不过以TCP和IP协议为代表。UDP(User Data Protocl),即用户数据报协议,是与TCP相对应的协议,它属于TCP/IP协议族中的一个。TCP/IP进行数据传输主要分为两个过程:建立连接过程和数据传输过程。
- TCP/IP协议通过三次握手建立一个可靠的连接,步骤:
- 第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
- 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
- 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
- 数据传输过程:发送端发送数据,然后进入等待ACK确定信号状态,随后接收端接收到数据,发送ACK确认信号,发送端接收到ACK后才发送下一组数据,同时发送端有一个定时器,定时时间到了没有接收到ACK,就认为发送失败了,进行重新发送。因为发送端发送完数据后处于等待状态,因此为了提高效率,引入“滑动窗口”的概念,就是发送的时候一次发送多组数据,相当于窗口的大小,然后当接收到第一个ACK后,就将窗口向后移动一个数据,就形成了滑动窗口的情况。
下表显示了协议间的关系:
2. Socket的基本概念
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口,在设计模式中,Socket就是一个门面模式,它把复杂的TCP/IP协议族隐藏在了Scoket接口的后面,让Socket去组织数据,以符合指定的协议。
举个简单的例子先:你要打电话给一个朋友,先拨号,朋友听到电话铃声后提取电话,这时你和你的朋友就建立起了连接,就可以讲话了,等交流结束,挂断电话结束此次交谈,这个生活场景的例子就解释了TCP/IP的工作原理和流程,其具体的流程图如下所示:
- 使用WSAStartup()函数检查系统协议栈安装情况
- 使用socket()函数创建服务器端通信套接口
- 使用bind()函数将创建的套接口与服务器地址绑定
- 使用listen()函数使服务器套接口做好接收连接请求准备
- 使用accept()接收来自客户端由connect()函数发出的连接请求
- 根据连接请求建立链接后,使用send()还念书发送数据,或者使用recv()函数接收数据
- 使用closesocket()函数关闭套接口(可以先用shutdown()函数吸纳关闭读写通道)
- 最后调用WSACleanup()函数结束Winsock Sockets API
socket的发送和接收缓存区是两个相互独立的缓存区。socket的send和recv函数分为阻塞模式的和非阻塞模式的,在定义socket的时候,默认设置为了阻塞模式,而后如果想要改变为非阻塞模式,只需使用ioctlsocket函数进行设置即可。非阻塞模式一旦调用即刻返回,因此如果传输数据较为稀疏,则会经常返回失败信息,为此一般使用循环来接收数据。而常用的方式是阻塞模式,send函数没啥问题,而recv函数一般配合select函数,可以做到虽然recv是阻塞的,但是合起来的效果是阻塞一段时间的方式。下面给出send和recv函数的工作流程。
- 阻塞模式下的send和recv函数
int send( SOCKET s,const char FAR *buf,int len,int flags );
不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。
客户程序一般用send函数向服务器发送请求,而服务器则通常用send函数来向客户程序发送应答。
该函数的第一个参数指定发送端套接字描述符;
第二个参数指明一个存放应用程序要发送数据的缓冲区;
第三个参数指明实际要发送的数据的字节数;
第四个参数一般置0。
这里只描述同步Socket的send函数的执行流程。当调用该函数时,send先比较待发送数据的长度len和套接字s的发送缓冲的长度,如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR;如果len小于或者等于s的发送缓冲区的长度,那么send先检查协议 是否正在发送s的发送缓冲中的数据,如果是就等待协议把数据发送完,如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,那么 send就比较s的发送缓冲区的剩余空间和len,如果len大于剩余空间大小send就一直等待协议把s的发送缓冲中的数据发送完,如果len小于剩余 空间大小send就仅仅把buf中的数据copy到剩余空间里(注意并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里)。如果send函数copy数据成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。
要注意send函数把buf中的数据成功copy到s的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。
注意:在Unix系统下,如果send在等待协议传送数据时网络断开的话,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。
int recv( SOCKET s,char FAR *buf,int len,int flags);
不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。
该函数的第一个参数指定接收端套接字描述符;
第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
第三个参数指明buf的长度;
第四个参数一般置0。
这里只描述同步Socket的recv函数的执行流程。当应用程序调用recv函数时,recv先等待s的发送缓冲 中的数据被协议传送完毕,如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR,如果s的发送缓冲中没有数 据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,只到 协议把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以 在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的),recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。
2. 非阻塞模式
非阻塞模式就是一旦调用,如果不满足copy条件,则函数马上返回失败信息。
5. Winsock编程流程(以服务器端为例)
1. 使用WSAStartup()装载检查TCP/IP协议
2. 创建套接字,有三种类型的套接字供选择:流式套接字(用于TCP),数据报套接字(UDP)和原始套接字(底层协议)
3. 将创建好的套接字和IP地址以及端口进行绑定
4. 使用getsockopt获得套接口的参数,使用setsockopt设置套接口的参数(主要设置套接口的收发缓存大小),使用ioctlsocket
设置阻塞还是非阻塞的模式(这里的阻塞和非阻塞模式指的是API函数是否调用完马上返回)
5. 进行通信,在通信的部分,windows socket给我们提供了五种I/O模型用于收发通信:(他们都可以和阻塞或非阻塞函数一起
使用)
a. select模型,常用的模型,以select函数为核心,可以监控套接口是否有数据收发。它是同步模型。
b. WSAAsyncSelect模型,异步I/O模型,利用windows的消息机制实现异步接收,需要有窗口接收消息,只有接收数据是
异步的。
c. WSAEventSelect 模型,异步I/O模型,通过事件触发来实现异步接收,它不需要接收窗口。
d. 重叠模型:比较完美的模型。
e. 完成端口:最为复杂的I/O模型,当需要一个进程管理很多的套接口的时候,很适合使用这种模型。
6. 使用closesocket()函数关闭套接口的收发。
7. 使用WSACleanup()函数卸载TCP/IP协议。