应用层通过传输层进行数据通信时,TCP和UDP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了称为套接字(Socket)的接口,区分不同应用程序进程间的网络通信和连接。
生成套接字,主要有3个参数:通信的目的IP地址、使用的传输层协议(TCP或UDP)和使用的端口号。Socket原意是“插座”。通过将这3个参数结合起来,与一个“插座”Socket绑定,应用层就可以和传输层通过套接字接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。
Socket可以看成在两个程序进行通讯连接中的一个端点,一个程序将一段信息写入Socket中,该Socket将这段信息发送给另外一个Socket中,使这段信息能传送到其他程序中。如图1:
Host A上的程序A将一段信息写入Socket中,Socket的内容被Host A的网络管理软件访问,并将这段信息通过Host A的网络接口卡发送到Host B,Host B的网络接口卡接收到这段信息后,传送给Host B的网络管理软件,网络管理软件将这段信息保存在Host B的Socket中,然后程序B才能在Socket中阅读这段信息。
要通过互联网进行通信,至少需要一对套接字,一个运行于客户机端,称之为ClientSocket,另一个运行于服务器端,称之为serverSocket。
根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个步骤:服务器监听,客户端请求,连接确认。
服务器监听:是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。
客户端请求:是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
连接确认:是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
套接字对象也有相应的方法,例如发送数据包的方法还有接收数据包的方法,介绍如下。
pubic void close() 当我们创建一个套接字后,用该方法关闭套接字。
public int getLocalPort() 返回本地套接字的正在监听的端口号。
public void receive(DatagramPacket p) 从网络上接收数据包并将其存储在DatagramPacket对象p中。p中的数据缓冲区必须足够大,receive()把尽可能多的数据存放在p中,如果装不下,就把其余的部分丢弃。接收数据出错时会抛出IOException异常。
public Void Send(DatagramPacket p) 发送数据包,出错时会发生IOException异常。
下面,我们详细解释在Java中实现客户端与服务器之间数据报通信的方法。
应用程序的工作流程如下:
(1)首先要建立数据报通信的Socket,我们可以通过创建一个DatagramSocket对象实现它,在Java中DatagramSocket类有如下两种构造方法:
public DatagramSocket() 构造一个数据报socket,并使其与本地主机任一可用的端口连接。若打不开socket则抛出SocketException异常。
public DatagramSocket(int port) 构造一个数据报socket,并使其与本地主机指定的端口连接。若打不开socket或socket无法与指定的端口连接则抛出SocketException异常。
(2)创建一个数据报文包,用来实现无连接的包传送服务。每个数据报文包用DatagramPacket类创建,DatagramPacket对象封装了数据报包数据、包长度、目标地址和目标端口。客户端要发送数据报文包,要调用DatagramPacket类以如下形式的构造函数创建DatagramPacket对象,将要发送的数据和包文目的地址信息放入对象之中。DatagramPacket(byte bufferedarray[],int length,InetAddress address,int port)即构造一个包长度为length的包传送到指定主机指定端口号上的数据报文包,参数length必须小于等于bufferedarry.length。
DatagramPacket类提供了4个类获取信息:
public byte[] getData() 返回一个字节数组,包含收到或要发送的数据报中的数据。
public int getLength() 返回发送或接收到的数据的长度。
public InetAddress getAddress() 返回一个发送或接收此数据报包文的机器的IP地址。
public int getPort() 返回发送或接收数据报的远程主机的端口号。
(3)创建完DatagramSocket和DatagramPacket对象,就可以发送数据报文包了。发送是通过调用DatagramSocket对象的send方法实现,它需要以DatagramPacket对象为参数,将刚才封装进DatagramPacket对象中的数据组成数据报发出。
(4)当然,我们也可以接收数据报文包。为了接收从服务器返回的结果数据报文包,我们需要创建一个新的DatagramPacket对象,这就需要用到DatagramPacket的另一种构造方式DatagramPacket(byte bufferedarray[],int length),即只需指明存放接收的数据报的缓冲区和长度。调用DatagramSocket对象的receive()方法完成接收数据报的工作,此时需要将上面创建的DatagramPacket对象作为参数,该方法会一直阻塞直到收到一个数据报文包,此时DatagramPacket的缓冲区中包含的就是接收到的数据,数据报文包中也包含发送者的IP地址,发送者机器上的端口号等信息。
(5)处理接收缓冲区内的数据,获取服务结果。
(6)当通信完成后,可以使用DatagramSocket对象的close()方法关闭数据报通信Socket。当然,Java会自动关闭Socket,释放DatagramSocket和DatagramPacket所占用的资源。但是作为一种良好的编程习惯,还是要显式地予以关闭。
与TCP协议发送和接收字节流不同,UDP终端交换的是一种称为数据报文的自包含(self-contained)信息。这种信息在Java中表示为DatagramPacket类的实例。发送信息时,Java程序创建一个包含了待发送信息的DatagramPacket实例,并将其作为参数传递给DatagramSocket类的send()方法。接收信息时,Java程序首先创建一个DatagramPacket实例,该实例中预先分配了一些空间(一个字节数组byte[]),并将接收到的信息存放在该空间中。然后把该实例作为参数传递给DatagramSocket类的receive()方法。
除传输的信息本身外,每个DatagramPacket实例中还附加了地址和端口信息,其具体含义取决于该数据报文是被发送还是被接收。若是要发送的数据报文, DatagramPacket实例中的地址则指明了目的地址和端口号,若是接收到的数据报文, DatagramPacket实例中的地址则指明了所收信息的源地址。因此,服务器端可以修改接收到的DatagramPacket实例的缓存区内容,再将这个实例连同修改后的信息一起,发回给它的源地址。在DatagramPacket的内部也有length和offset字段,分别定义了数据信息在缓存区的起始位置和字节数。请参考下面的介绍和第2.3.4节的内容,以避免在使用DatagramPackets时易犯的一些错误。
DatagramPacket: 创建
DatagramPacket(byte[ ] data, int length) DatagramPacket(byte[ ] data, int offset, int length) DatagramPacket(byte[ ] data, int length, InetAddress remoteAddr, int remotePort) DatagramPacket(byte[ ] data, int offset, int length, InetAddress remoteAddr, int remotePort) DatagramPacket(byte[ ] data, int length, SocketAddress sockAddr) DatagramPacket(byte[ ] data, int offset, int length, SocketAddress sockAddr) |
以上构造函数都创建一个数据部分包含在指定的字节数组中的数据报文,前两种形式的构造函数主要用来创建接收的端的DatagramPackets实例,因为没有指定其目的地址(尽管可以通过setAddress() 和setPort()方法,或setSocketAddress()方法来指定)。后四种形式主要用来创建发送端的DatagramPackets实例。
如果指定了offset,数据报文的数据部分将从字节数组的指定位置发送或接收数据。length参数指定了字节数组中在发送时要传输的字节数,或在接收数据时所能接收的最多字节数。length参数可能比data.length小,但不能比它大。
目的地址和端口号可以分别设置,或通过SocketAddress同时设置。
DatagramPacket: 地址处理
InetAddress getAddress() void setAddress(InetAddress address) int getPort() void setPort(int port) SocketAddress getSocketAddress() void setSocketAddress(SocketAddress sockAddr) |
除了构造函数外,以上方法提供了另外一些方法来访问和修改DatagramPacket实例的地址信息。另外需要注意,DatagramSocket的receive()方法是将其地址和端口设置为数据报发送者的地址和端口。
DatagramPacket: 处理数据
int getLength() void setLength(int length) int getOffset() byte[ ] getData() void setData(byte[ ] data) void setData(byte[ ] buffer, int offset, int length) |
前两个方法返回和设置数据报文中数据部分的内部长度。此内部长度可以通过其构造函数或setLength()方法显式地设定。若试图将其设置得比相关联的缓存区长度更大,程序将抛出一个IllegalArgumentException异常。DatagramSocket类的receive()方法在两个方面使用内部长度:在输入时,用来指定接收到的将被复制到缓冲区的消息的最长字节数,在返回时,用来指示实际存入缓冲区的字节数。
getOffset()方法返回发送或接收的数据存放在缓存区时的偏移量。不存在setOffset()方法,不过可以使用setData()方法来设置偏移量。
getData()方法返回与数据报文相关联的字节数组。实际返回的是对与DatagramPacket最近关联的字节数组的一个引用,而关联则是通过构造函数或setData()方法形成。返回的缓存数组的长度可能比数据报文内部长度更长,因此,必须使用内部长度和偏移量来指定实际接收到的信息。
setData()方法指定一个字节数组作为该数据报文的数据部分。第一种形式将整个字节数组作为缓冲区;第二种形式把字节数组中,从offset到offset+length-1的部分作为缓存区。每次调用第二种形式的setData()方法,都将更新数据的内部偏移量和长度。
套接字是通信的基础,是支持TCP/IP协议的网络通信的基本操作单元。可以将套接字看作不同主机间的进程进行双向通信的端点,它构成了单个主机内及整个网络间的编程界面。套接字存在于通信域中,通信域是为了处理一般的线程通过套接字通信而引进的一种抽象概念。套接字通常和同一个域中的套接字交换数据(数据交换也可能穿越域的界限,但这时一定要执行某种解释程序)。各种进程使用这个相同的域互相之间用Internet协议簇来进行通信。
套接字可以根据通信性质分类,这种性质对于用户是可见的。应用程序一般仅在同一类的套接字间进行通信。不过只要底层的通信协议允许,不同类型的套接字间也照样可以通信。套接字有两种不同的类型:流套接字和数据报套接字。
套接字工作原理:
要通过互联网进行通信,你至少需要一对套接字,其中一个运行于客户机端,我们称之为ClientSocket,另一个运行于服务器端,我们称之为ServerSocket。
根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个步骤:服务器监听,客户端请求,连接确认。
【所谓服务器监听】是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。
【所谓客户端请求】是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。
【所谓连接确认】是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。
通过前人的总结和描述,我们可以把该过程想像为一个简单的电话通讯过程;
[define]
#define 套接字的连接 电话的之间的连接
#define 服务端 提供XX节目在线有奖竞答的电话号码(ip:port)和接听服务人员(logic)
#define 客户端 关注XX节目(知道竞答电话[ip:port])并有可能打电话给XX节目答题的观众
[init]
1.如果该XX节目想继续办下去,就需要开通一个或者一组服务电话,开始等待观众打电话;
-->如果要实现C/S结构通信,需要开启一台或者多台服务端,绑定ip和port,并处于实时监听状态,等待未知客户接入;
2.如果一个观众希望和XX节目建立通信答题,需要知道XX节目的电话号码;
-->如果客户端希望与服务器建立连接,就需要知道服务器的ip和port;
[function]
3.接下来观众决定打电话给XX节目进行答题了,拨号...
-->接下来客户端开始连接服务器,通过服务器提供的ip和port连接服务端服务套接字..
4.一直在后台等待观众拨入的服务电话开始鸣响,分配一名服务人员与其交流,开始忽悠...;其他服务电话组依然等待其他观众接入电话
-->一直处于等待的服务套接字接收到客户端套接字连接申请,为其开启一个服务线程(防止再有用户接入) 提供服务,再回到等待用户接入状态..