socket概述

DinS          Written on 2017/9/12

一、Socket概述

现在无疑是互联网的时代,一个应用程序或多或少都会有网络功能,但是互联网通信是由众所周知的TCP/IP五层结构组织起来的:应用层/传输层/网络层/数据链路层/物理层。作为一个程序员,不可能掌握所有5个层次

实际上,我们关注的主要是在语义上利用互联网收发信息。所谓socket,就是一个网络通信API。其提供了一系列接口,程序通过调用这些接口达到网络通信的目的。

socket起源于上世纪70年代,由加州伯克利大学建立,采用C语言编写,用于计算机网络通信,最初是为Unix系统开发的。这个称为BSD(Berkeley sockets)。当时也有其他的通信库,不过BSD逐渐胜出并成为了公认的互联网通信标准接口。到了九十年代初,由Microsoft联合了其他几家公司共同制定了一套 WINDOWS下的网络编程接口,即Windows Sockets规范。后来该规范又从1.1版本升级为2.0,这个也是<Winsock2.h>中“2”的来源。

在进入具体的介绍之前,有必要澄清一些事情。
socket仅仅是一组API,具体实现由操作系统提供。在使用socket时,或多或少会用到操作系统的东西,这对可移植性提出了重大挑战。
遗憾的是C++并没有在语言层面提供socket,windows下使用C++编写socket通常跟Winsock2结合,这限制了程序的适用范围。相比之下Java在语言层面有socket,这是为什么好多人用java做网络相关app。

但是还是有补救措施的,有许多开源库可以提供语言层面的socket,这个会在另一个专题说明。本专题将以Winsock2为例,在介绍socket时着重强调代码背后的原理和思路,具体写法不会太过深入。当读者理解了背后的原理后,再去阅读另一个专题《asio基础使用》,使用那个库可以写出现代C++风格的、跨平台的网络通信程序。

再次强调一遍:不要使用winSock开发,对于本文内容只要理解背后的原理即可。

二、Socket要素

试想你跟一个朋友的一次对话。
A:“听说最近你中奖了,真的假的?”
B:“中了500万,你信吗?”
A:“啥都不说,见面分一半。”

从概念上讲,这个可以称之为“通信”,虽然生活中我们把这个称为聊天。
现在的问题是,这里面有哪些要素使得这次通信得以成功?

1.对象目标
首先必须有A、B这两个人才能够对话,不然通信就没有意义。转换成计算机术语,至少得有两个进程来进行通讯。
在socket中一方称为服务端(server),另一方称为客户端(client)。最简单的情况就是一对一,跟上面A和B一样。更复杂的情况有多对一,这是设计服务器时必须处理的问题。

由此引出另一个问题,即如何在网络中唯一标识一个进程。
A要跟B说话,而不是跟C。生活中可以看脸,网络中如何区别呢?
答案是使用“三元组”:地址、协议、端口。
地址指ip地址,可以唯一标识网络中的主机。协议+端口可以唯一标识主机中的进程。

ip地址就不多说了。
端口是个什么东西?端口是一个抽象的软件结构,其中包含了IO缓存区。Socket通过绑定一个端口来实现收发消息。
协议又是什么?见下。

2.沟通规范
A和B之所以能够发生有意义的交流,一个必要条件是双方都说同一种语言。也就是说,交流需要一种规范,否则就是无意义的,双方不在一个频道上。
在socket中,存在两种常用规范,其实也就是协议:TCP协议和UDP协议。
TCP协议(Transmission Control Protocol)也可称为流式Socket,其特点是面向连接。换成通俗的话就是在A、B之间先建立可靠的连接,再传输消息。
UDP协议(User Datagram Protocol)也称报文式Socket,其特点是非面向连接。通俗地说就是不确认对方是否能够收到,直接发送消息。

凭直觉可以知道,TCP肯定要更复杂一点,因为涉及到确认连接。
这其中涉及到著名的“三次握手”。稍后会说明。

3.语法语序
在满足前两条后,信息传递没什么问题了。
但是这里面还有一个细节问题,就是语法和语序。假设B这么说:“你信500万,中了吗?”内容是一样的,只不过顺序变了一下,有可能产生歧义或者干脆没法理解。

计算机通信中也存在这个问题,就是字节序的问题。对于不同的CPU,字节在内存中有不同的保存顺序,就是平常说的大端小端模式。

而对于socket传输,规定好是按大端传输的。于是如果不做调整直接使用,有可能悲剧。这个问题在绑定端口的时候会解决掉。

三、Socket基础:UDP

说了那么多概念,下面来干些实事。最权威的使用Winsock2的方法应该是微软自己写的教程了,见https://msdn.microsoft.com/en-us/library/ms738545(v=vs.85).aspx。建议读者自己去参考。
由于UDP比较简单,所以先介绍UDP。重点在理解工作原理和流程。

这部分是服务端的代码,有点长,一半都是注释,剩下的套路居多

点这里看大图

winsock属于C风格,看起来略显凌乱。核心的东西都在注释上了,按照套路写没什么问题,这里要强调的是整个过程中最核心的部分。
我们建立了一个socket,同时建立了一个对应的地址结构,地址结构中处理了字节序问题。然后我们将该socket和地址结构绑定。因为地址结构中记录了端口号,所以实际上就是将socket和端口绑定。
绑定完成后就开始等待接收信息,UDP可以这样TCP不行,因此这里用UDP方便讲解。需要注意的是用于接收信息的函数recvfrom会阻塞进程,实际上服务器设计基本上都是围绕这个阻塞问题展开的。
接收完之后又给客户端发送了一条消息,但是这个已经不是核心了,UDP不管对方是否收到,发送完就返回。
对于这样一个过程要在概念上充分理解。

服务端就是这样了,下面看客户端,要简单一点。

大图点这里

与服务端大同小异,有两处需要强调。
第一,客户端没有绑定这个步骤。建立完socket直接发送。为什么没有?因为客户端不需要监听来自其他ip地址的信息,所以系统会自动分配一个。
第二,客户端的地址结构中ip地址那一项需要显式指明。在实际中通常使用127.0.0.1调试,发布时把这个改成服务端ip地址,就能发送给服务端了。但是这里有个坑,从代码层面来说就是这样的,但是需要记住:服务端的环境是需要配置的。也就是说,你直接把服务端的exe放到一个家用PC上打开,是收不到客户端的信息的。在服务器环境配置好后服务端的socket才能正常工作,客户端倒是用不着。
至于怎么配置环境,见《服务器环境搭建》。

运行结果如下:

打开服务端,什么都不显示,因为调用了recvfrom阻塞了进程。然后打开客户端。客户端给服务端sendto,因此服务端显示信息。客户端调用recvfrom阻塞。

按任意键后服务端给客户端sendto,然后不管是否成功就return 0退出了。客户端收到服务端信息,显示内容。

四、Socket基础:TCP

在理解了UDP后,处理TCP就应该不难了。TCP与UDP的重要区别是面向连接。为了建立可靠的连接,需要额外的一步,这个额外的一步,就是“三次握手”。
为什么叫“三次握手”?让我们来进行一个思想实验,看看建立可靠连接需要哪些条件。

假设现在A集团军和B集团军准备围攻一个山头,A在山的北面驻扎,B在山的南面驻扎,双方靠无线电通信,但是无线电并不稳定,有干扰。
根据战前评估,如果A或者B单方面进攻,将遭受重大损失,且无法攻占山头,只有A和B同时发起进攻,才能攻占山头。很显然,要完成任务,A和B必须先沟通清楚进攻的时间。
现在A向B发送了一条信息:明天上午9点开始进攻。这个可靠吗?不可靠,这一条信息就跟UDP一样,不一定能够送达。如果没有送达,则A单方面发动进攻,B不知情,任务失败。
A应该怎么办?显然需要等待B的回复。如果收到了B的回复并且B同意了,那么A可以放心地进攻。
但是对于B来说并没有这么简单。试想B收到了A的情报:明天上午9点开始进攻,然后B回复A:收到,明天上午9点开始进攻。B应该如期进攻吗?因为通信是不稳定的,B发送的信息A可能收不到。
对于A而言,发送了一条信息给B,结果B没有回应,那么A认为B没收到,所以不会在明天早上9点进攻。但是B实际上收到了A的消息,只是回复确认没有发送给A。如果B如期进攻,则A不会行动,同样任务失败。因此对于B而言,必须收到来自A的回复,才能进攻。
整理一下过程:A发送信息给B->B接收信息回复A->A发送信息回复B。
这样下来通信必定是可靠的,双方都会在明天早上9点开始进攻。

这样的一个过程,抽象出来就是“三次握手”。

图片来源:http://www.cnblogs.com/skynet/archive/2010/12/12/1903949.html

在理解了“三次握手”后,TCP也没有什么难度了,更何况不需要程序员干预这个过程,调API即可。下面是TCP下服务端的代码。

(大图点这里)

还是挺长的,不过注释占一半,剩下的大部分跟UDP比较像,重点看区别。
TCP比UDP多了一个步骤:listen->accept,这部分就是“三次握手”发生的阶段,listen让服务器socket监听端口,然后accept等待客户端连接。尤其注意accept成功后发生的事情:返回了一个新socket,称为连接socket,这一点跟UDP截然不同。
之后的send和recv都是通过这个新的连接socket进行的,我们不需要额外指定地址结构。而对于UDP我们则是保存了客户端地址结构,然后使用sendto显式指定传送对象。

接下来看看客户端的代码:

(大图点这里)

客户端的逻辑基本跟UDP类似,不同之处在于需要调用connect连接服务端。
连接成功之后通过客户端socket直接send和recv即可。

运行结果:

打开服务端的显示,此时accept阻塞进行,服务端等待连接。

打开客户端,connect成功,双方收发消息。

五、小结

至此我们已经掌握了UDP和TCP最基本的一对一模式,由此可见socket并不困难,大部分都是套路。我们完全可以把刚刚的这个程序扩展为一个对话软件,你一句我一句。
取得的成果是令人欣慰的,然而仔细一考虑这个程序并不能满足实际网络通信的需要,为什么呢?一个明显的缺点是只能依次说话,只能A说完B再说,然后A再说,这样很不方便。
但更为严重的问题是,实际的运行环境中不可能只有一个客户端,一旦A客户端连接进入后,服务端就没法再响应其他客户端了。那么增加recvfrom/recv不是就可以了吗?但是记住recvfrom/recv是阻塞的,也就是说服务端在等待其他客户端连接时不能干其他的事,那么已经连接上的客户端怎么办?

由此可见socket真正的难点是设计:如何做到多对一响应。其实仔细分析一下,可以发现问题的症结就在recvfrom/recv是阻塞的,围绕这个问题演进出了几种设计socket的模式。
这里要说明的是,网络中大部分socket都是用TCP来做的,实际上这也可以理解,连接可靠性是基础。之后都将以TCP为例做讲解。

几种服务器模式将分开来讲解:《服务器模式:阻塞+多线程》、《服务器模式:非阻塞+轮询》、《服务器模式:多路复用》,重点是理解思路,真正写代码还是建议用第三方库,比如ASIO,可见《asio服务器模式:多线程或轮询》和《asio服务器模式:异步调用》。

 

参考资料:http://www.cnblogs.com/skynet/archive/2010/12/12/1903949.html
http://blog.csdn.net/beyond_cn/article/details/10033541
http://blog.csdn.net/hguisu/article/details/7453390