Java_Socket编程
网络基础知识
所谓计算机网络,就是把分布在不同地理区域的计算机与专门的外部设备用通信线路互连成一个规模大、功能强的网络系统,从而使众多的计算机可以方便地互相传递信息,共享硬件、软件、数据信息等资源。
通过计算机网络可以向全社会提供各种经济信息、科研情报和咨询服务。其中,国际互联网Internet上的全球信息网(WWW,World Wide Web)服务就是一个最典型也是最成功的例子。实际上,今天的网络承载绝大部分大型企业的运转,一个大型的、全球性的企业或组织的日常工作流程都是建立在互联网基础之上的。计算机网络的品种很多,根据各种不同的分类原则,可以得到各种不同类型的计算机网络。计算机网络通常是按照规模大小和延伸范围来分类的,常见的划分为:局域网(LAN)、城域网(MAN)、广域网(WAN)。Internet可以视为世界上最大的广域网。
如果按照网络的拓扑结构来划分,可以分为星型网络、总线网络、环线网络、树型网络、星型环线网络等;如果按照网络的传输介质来划分,可以分为双绞线网、同轴电缆网、光纤网和卫星网等。
计算机网络中实现通信必须有一些约定,这些约定被称为通信协议。通信协议负责对传输速率、传输代码、代码结构、传输控制步骤、出错控制等制定处理标准。为了让两个节点之间能进行对话,必须在它们之间建立通信工具,使彼此之间能进行信息交换。
开放系统互连参考模型把计算机网络分成物理层、数据链路层、网络层、传输层、会话层、表示层、应用层七层,受到计算机界和通信业的极大关注。通过十多年的发展和推进,OSI模式已成为各种计算机网络结构的参考标准。
- 应用层:应用层(application-layer)的任务是通过应用进程间的交互来完成特定网络应用。应用层协议定义的是应用进程(进程:主机中正在运行的程序)间的通信和交互的规则。对于不同的网络应用需要不同的应用层协议。在互联网中应用层协议很多,如域名系统 DNS,支持万维网应用的 HTTP 协议,支持电子邮件的 SMTP 协议等等。我们把应用层交互的数据单元称为报文。
- 传输层:运输层(transport layer)的主要任务就是负责向两台主机进程之间的通信提供通用的数据传输服务。应用进程利用该服务传送应用层报文。“通用的”是指并不针对某一个特定的网络应用,而是多种应用可以使用同一个运输层服务。由于一台主机可同时运行多个线程,因此运输层有复用和分用的功能。所谓复用就是指多个应用层进程可同时使用下面运输层的服务,分用和复用相反,是运输层把收到的信息分别交付上面应用层中的相应进程。
- 网络层:在计算机网络中进行通信的两个计算机之间可能会经过很多个数据链路,也可能还要经过很多通信子网。网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。 在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送。在 TCP/IP 体系结构中,由于网络层使用 IP 协议,因此分组也叫 IP 数据报 ,简称数据报。
- 数据链路层:数据链路层(data link layer)通常简称为链路层。两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。 在两个相邻节点之间传送数据时,数据链路层将网络层交下来的 IP 数据报组装成帧,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息(如同步信息,地址信息,差错控制等)。
- 物理层:在物理层上所传送的数据单位是比特。物理层(physical layer)的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异, 使其上面的数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说,这个电路好像是看不见的。
TCP/IP协议
IP 协议则是一种非常重要的通信协议。IP(Internet Protocol)协议又称互联网协议,是支持网间互联的数据报协议。它提供网间连接的完善功能,包括IP数据报规定互联网络范围内的地址格式。
经常与IP协议放在一起的还有TCP(Transmission Control Protocol)协议,即传输控制协议,它规定一种可靠的数据信息传递服务。虽然IP和TCP这两个协议功能不尽相同,也可以分开单独使用,但它们是在同一个时期作为一个协议来设计的,并且在功能上也是互补的。因此实际使用中常常把这两个协议统称为TCP/IP协议,TCP/IP协议最早出现在UNIX操作系统中,现在几乎所有的操作系统都支持TCP/IP协议,因此TCP/IP协议也是Internet中最常用的基础协议。
计算机与网络设备要相互通信,双方就必须基于相同的方法。比如,如何探测到通信目标、由哪一边先发起通信、使用哪种语言进行通信、怎样结束通信等规则都需要事先确定。不同的硬件、操作系统之间的通信,所有的这一切都需要一种规则。而我们就把这种规则称为协议(protocol)。
TCP在传输之前会进行三次沟通,一般称为“三次握手”,传完数据断开的时候要进行四次沟通,一般称为“四次挥手”。要理解这个过程首先需要理解TCP中的两个序号和三个标志位的含义:
- seq:sequence number的缩写,表示所传数据的序号。TCP传输时每一个字节都有一个序号,发送数据时会将数据的第一个序号发送给对方,接收方会按序号检查是否接收完整了,如果没接收完整就需要重新传送,这样就可以保证数据的完整性。
- ack:acknoledgement number的缩写,表示确认号。接收端用它来给发送端反馈已经成功接收到的数据信息的,它的值为希望接收的下一个数据包起始序号,也就是ack值所代表的序号前面数据已经成功接收到了。
- ACK:确认位,只有ACK=1的时候ack才起作用。正常通信时ACK为1,第一次发起请求时因为没有需要确认接收的数据所以ACK为0。
- SYN:同步位,用于在建立连接时同步序号。刚开始建立连接时并没有历史接收的数据,所以ack也就没办法设置,这时按照正常的机制就无法运行了,SYN的作用就是来解决这个问题的,当接收端接收到SYN=1的报文时就会直接将ack设置为接收到的seq+1的值,注意这里的值并不是校验后设置的,而是根据SYN直接设置的,这样正常的机制就可以运行了,所以SYN叫同步位。需要注意的是,SYN会在前两次握手时都为1,这是因为通信的双方的ack都需要设置一个初始值。
- FIN:终止位,用来在数据传输完毕后释放连接。
整个传输过程如图所示。
图中上部为三次握手,下部为四次挥手,这里的四次挥手中画的是客户端提出的终止连接,在实际传输过程中也有可能是服务端提出终止连接,它们的处理过程都是一样的。TCP的传输是双全工模式,也就是说传输的双方是对等的,可以同时传输数据,所以无论连接还是关闭都需要对双方同时进行。三次握手中前两次可以保证服务端可以正确接收并返回请求,后两次可以保证客户端可以正确接收并返回请求,而且在三次握手的过程中还使用SYN标志初始化了双方的ack值。四次挥手就是双方分别发送FIN标志来关闭连接并让对方确认。四次挥手过程如下:
- 客户端-发送一个 FIN,用来关闭客户端到服务器的数据传送
- 服务器-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加 1 。和 SYN 一样,一个 FIN 将占用一个序号
- 服务器-关闭与客户端的连接,发送一个 FIN 给客户端
- 客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加 1
IP地址和端口号
IP 地址用于唯一地标识网络中的一个通信实体,这个通信实体既可以是一台主机,也可以是一台打印机,或者是路由器的某一个端口。而在基于IP协议网络中传输的数据包,都必须使用IP地址来进行标识。
IP地址是数字型的,IP地址是一个32位(32bit)整数,但通常为了便于记忆,通常把它分成4个8位的二进制数,每8位之间用圆点隔开,每个8位整数可以转换成一个0~255的十进制整数,因此我们看到的IP地址常常是这种形式:202.9.128.88。
IP地址被分成了A、B、C、D、E五类,每个类别的网络标识和主机标识各有规则。
- A类:10.0.0.0 ~ 10.255.255.255
- B类:172.16.0.0 ~ 172.31.255.255
- C类:192.168.0.0 ~192.168.255.255
IP 地址用于唯一地标识网络上的一个通信实体,但一个通信实体可以有多个通信程序同时提供网络服务,此时还需要使用端口。端口是一个16位的整数,用于表示数据交给哪个通信程序处理。端口号可以从0到65535。
InetAddress
Java提供了InetAddress类来代表IP地址,InetAddress下还有两个子类:Inet4Address、Inet6Address,它们分别代表Internet Protocol version 4(IPv4)地址和Internet Protocol version 6(IPv6)地址。
InetAddress类没有提供构造器,而是提供了如下两个静态方法来获取InetAddress实例。
- getByName(String host):根据主机获取对应的InetAddress对象。
- getByAddress(byte[]addr):根据原始IP地址来获取对应的InetAddress对象。
InetAddress还提供了如下三个方法来获取InetAddress实例对应的IP地址和主机名。
- String getCanonicalHostName():获取此 IP 地址的全限定域名。
- String getHostAddress():返回该InetAddress实例对应的IP地址字符串(以字符串形式)。
- String getHostName():获取此 IP 地址的主机名。
除此之外,InetAddress 类还提供了一个 getLocalHost()方法来获取本机 IP 地址对应的InetAddress实例。
下面通过示例来学习InetAddress的用法:
1 | package cn.bytecollege; |
1 | package cn.bytecollege; |
基于TCP协议的网络编程
TCP/IP通信协议是一种可靠的网络协议,它在通信的两端各建立一个Socket,从而在通信的两端之间形成网络虚拟链路。一旦建立了虚拟的网络链路,两端的程序就可以通过虚拟链路进行通信。Java对基于TCP协议的网络通信提供了良好的封装,Java使用Socket对象来代表两端的通信端口,并通过Socket产生IO流来进行网络通信。
ServerSocket创建服务端
Java中能接收其他通信实体连接请求的类是ServerSocket,ServerSocket对象用于监听来自客户端的 Socket 连接,如果没有连接,它将一直处于等待状态。ServerSocket 包含一个监听来自客户端连接请求的方法。
- Socket accept():如果接收到一个客户端Socket的连接请求,该方法将返回一个与客户端Socket对应的Socket;否则该方法将一直处于等待状态,线程也被阻塞。
ServerSocket类提供了如下几个构造器。
- ServerSocket(int port):用指定的端口port来创建一个ServerSocket。该端口应该有一个有效的端口整数值,即0~65535。
- ServerSocket(int port,int backlog):增加一个用来改变连接队列长度的参数backlog。
- ServerSocket(int port,int backlog,InetAddress localAddr):在机器存在多个 IP地址的情况下,允许通过localAddr参数来指定将ServerSocket绑定到指定的IP地址。
当ServerSocket使用完毕后,应使用ServerSocket的close()方法来关闭该ServerSocket。
下面我们使用ServerSocker创建服务端:
1 | package cn.bytecollege.socket; |
18.3.2 使用Socket通信
客户端通常可以使用Socket的构造器来连接到指定服务器,Socket通常可以使用如下两个构造器。
- Socket(InetAddress/String remoteAddress, int port):创建连接到指定远程主机、远程端口的Socket,该构造器没有指定本地地址、本地端口,默认使用本地主机的默认IP地址,默认使用系统动态分配的端口。
- Socket(InetAddress/String remoteAddress, int port, InetAddresslocalAddr, int localPort):创建连接到指定远程主机、远程端口的Socket,并指定本地IP地址和本地端口,适用于本地主机有多个IP地址的情形。
下面通过示例来学习Socket的使用
1 | package cn.bytecollege.socket; |
当客户端、服务器端产生了对应的Socket之后,就得到了如图12.4所示的通信示意图,程序无须再区分服务器端、客户端,而是通过各自的Socket进行通信。Socket提供了如下两个方法来获取输入流和输出流。
- InputStream getInputStream():返回该Socket对象对应的输入流,让程序通过该输入流从Socket中取出数据。
- OutputStream getOutputStream():返回该Socket对象对应的输出流,让程序通过该输出流向Socket中输出数据。
接下来使用Socket和ServerSocket进行简单的通信。
第一步:新建客户端
1 | package cn.bytecollege.socket; |
第二步:新建服务端
1 | package cn.bytecollege; |
在上例中MyClient内,创建Socket对象用于连接服务端,其中127.0.0.1指本机IP,10086则是服务器的端口号,为了便于打印服务端响应的数据,将服务端响应的输入字节流包装成字符流。接受到服务端响应的数据后进行打印。
在服务端,使用accept()方法监听客户端的请求,当请求到达后,获取输出流,对客户端进行响应。
Socket连续通信
在上一小节的示例中示范了客户端和服务端之间的简单通信,但是发现客户端无法和服务端连续通信,也就是说在客户端和服务端通信后客户端就结束了,在本小节中将以简单的聊天室为例学习Socket的连续通信。
首先新建服务端:
1 | package cn.bytecollege.server; |
在上例代码中,将每个服务端的接收到的Socket都放入集合中,并且为这个Socket启动线程,在线程内将客户端发送的数据响应给所有已经连接的Socket。服务端线程代码如下:
1 | package cn.bytecollege.server; |
从整体来看服务端所做的事情就是接收客户端请求并将客户端发送来的数据向所有已经连接的Socket进行发送。接下来,编写客户端代码。
1 | package cn.bytecollege.client; |
在客户端内当连接到服务器后启动线程循环接收从服务端发送的数据,然后循环接收从键盘输入的数据,并发送给服务器。由服务器转发给所有客户端。客户端线程代码如下:
1 | package cn.bytecollege.client; |
需要注意的是在整个过程中并没有关闭流,因为一旦关闭流以后再去接收和发送数据将会抛出异常。
基于UDP协议的网络编程
UDP 协议是一种不可靠的网络协议,它在通信实例的两端各建立一个 Socket,但这两个 Socket 之间并没有虚拟链路,这两个 Socket 只是发送、接收数据报的对象。Java 提供了DatagramSocket 对象作为基于 UDP协议的 Socket,使用 DatagramPacket 代表 DatagramSocket 发送、接收的数据报。
UDP 协议是英文 User Datagram Protocol 的缩写,即用户数据报协议,主要用来支持那些需要在计算机之间传输数据的网络连接。UDP协议目前应用不如 TCP 协议广泛,但 UDP 协议依然是一个非常实用和可行的网络传输层协议。尤其是在一些实时性很强的应用场景中,比如网络游戏、视频会议等,
UDP 协议是一种面向非连接的协议,面向非连接指的是在正式通信前不必与对方先建立连接,不管对方状态就直接发送。至于对方是否可以接收到这些数据内容,UDP 协议无法控制,因此说 UDP协议是一种不可靠的协议。UDP 协议适用于一次只传送少量数据、对可靠性要求不高的应用环境。
与前面介绍的 TCP 协议一样,UDP 协议直接位于IP 协议之上。实际上,IP 协议属于 OSI参考模型的网络层协议,而 UDP 协议和 TCP 协议都属于传输层协议。
因为 UDP 协议是面向非连接的协议,没有建立连接的过程,因此它的通信效率很高;但也正因为如此,它的可靠性不如 TCP 协议。
UDP 协议的主要作用是完成网络数据流和数据报之间的转换——在信息的发送端,UDP 协议将网络数据流封装成数据报,然后将数据报发送出去;在信息的接收端,UDP 协议将数据报转换成实际数据内容。
使用DatagramSocket发送接收数据
Java 使用DatagramSocket 代表 UDP协议的 Socket,
它的唯一作用就是接收和发送数据报,Java 使用 DatagramPacket 来代表数据报, DatagramSocket 接收和发送的数据都是通过 DatagramPacket 对象完成的。
DatagramScoket包含如下三个构造器:
- DatagramSocket()∶创建一个 DatagramSocket 实例,并将该对象绑定到本机默认 IP 地址、本机 所有可用端口中随机选择的某个端口。
- DatagramSocket(int prot)∶ 创建一个 DatagramSocket 实例,并将该对象绑定到本机默认 IP 地址、指定端口。
- DatagramSocket(int port,InetAddres laddr)∶ 创建一个 DatagramSocket 实例,并将该对象绑定到 指定 IP地址、指定端口。
通过上面三个构造器中的任意一个构造器即可创建一个 DatagramSocket 实例,通常在创建服务器时,创建指定端口的 DatagramSocket 实例——这样保证其他客户端可以将数据发送到该服务器。一旦得到了 DatagramSocket 实例之后,就可以通过如下两个方法来接收和发送数据。
- receive(DatagramPacket p)∶ 从该 DatagramSocket 中接收数据报。
- send(DatagramPacket p)∶以该 DatagramSocket 对象向外发送数据报。
从上面两个方法可以看出,使用 DatagramSocket 发送数据报时,DatagramSocket 并不知道将该数据报发送到哪里,而是由 DatagramPacket 自身决定数据报的目的地。
下面看一下 DatagramPacket 的构造器。
- DatagramPacket(byte[] buf,int length)∶以一个空数组来创建 DatagramPacket 对象,该对象的作用 是接收 DatagramSocket 中的数据。
- DatagramPacket(byte[] buf,int length,InetAddress addr, int port)∶ 以一个包含数据的数组来创建 DatagramPacket 对象,创建该DatagramPacket 对象时还指定了IP 地址和端口——这就决定了该数据报的目的地。
- DatagramPacket(byte[] buf, int offset, int length)∶ 以一个空数组来创建 DatagramPacket 对象,并 指定接收到的数据放入 buf 数组中时从 offset 开始,最多放 length 个字节。
- DatagramPacket(byte[] buf,int offset, int length,InetAddress address,int port)∶创建一个用于发送的 DatagramPacket 对象,指定发送 buf 数组中从 offset 开始,总共 length 个字节。
下面程序使用DatagramSocket实现服务端和客户端的网络通信。
首先编写服务端
1 | package cn.bytecollege.server; |
从代码中可以看出使用DatagramSocket编程相对简单,需要注意的是客户端和服务端发送和接收数据都必须放在数据包中,也就是说需要创建DatagramPacket对象。接下来编写客户端。
1 | package cn.bytecollege.client; |