现代的应用程序都离不开网络,网络编程是非常重要的技术。Java SE提供java.net包,其中包含了网络编程所需要的最基础一些类和接口。这些类和接口面向两个不同的层次:基于Socket的低层次网络编程和基于URL的高层次网络编程,所谓高低层次就是通信协议的高低层次,Socket采用TCP、UDP等协议,这些协议属于低层次的通信协议;URL采用HTTP和HTTPS这些属于高层次的通信协议。低层次网络编程,因为它面向底层,比较复杂,但是“低层次网络编程”并不等于它功能不强大。恰恰相反,正因为层次低,Socket编程与基于URL的高层次网络编程比较,能够提供更强大的功能和更灵活的控制,但是要更复杂一些。
## 24.1 网络基础
**1 客户端服务器结构网络**
客户端服务器(Client Server,缩写C/S)结构网络,是一种主从结构网络。如图24-1所示,服务器一般处于等待状态,如果有客户端请求,服务器响应请求建立连接提供服务。服务器是被动的,有点像在餐厅吃饭时候的服务员。而客户端是主动的,像在餐厅吃饭的顾客
![](https://box.kancloud.cn/8e5d2dcf152ffb19e141ef2927c28e89_925x586.png)
例如:Web服务、文件传输服务和邮件服务等。虽然它们存在的目的不一样,但基本结构是一样的。这种网络结构与设备类型无关,服务器不一定是电脑,也可能是手机等移动设备。
**2 对等结构网络**
对等结构网络也叫点对点网络(Peer to Peer,缩写P2P),每个节点之间是对等的。它们如图24-2所示,每个节点既是服务器又是客户端,这种结构有点像吃自助餐。
![](https://box.kancloud.cn/723209bd11bb7d004377fa85c17af00f_868x552.png)
对等结构网络分布范围比较小。通常在一间办公室或一个家庭内,因此它非常适合于移动设备间的网络通讯,网络链路层是由蓝牙和WiFi实现。
### 24.1.2 TCP/IP协议
网络通信会用到协议,其中TCP/IP协议是非常重要的。TCP/IP协议是由IP和TCP两个协议构成的,IP(Internet Protocol)协议是一种低级的路由协议,它将数据拆分成许多小的数据包中,并通过网络将它们发送到某一特定地址,但无法保证都所有包都抵达目的地,也不能保证包的顺序。
由于IP协议传输数据的不安全性,网络通信时还需要TCP协议,传输控制协议(Transmission Control Protocol,TCP)是一种高层次的协议,面向连接的可靠数据传输协议,如果有些数据包没有收到会重发,并对数据包内容准确性检查并保证数据包顺序,所以该协议保证数据包能够安全地按照发送时顺序送达目的地。
### 24.1.3 IP地址
为实现网络中不同计算机之间的通信,每台计算机都必须有一个与众不同的标识,这就是IP地址,TCP/IP使用IP地址来标识源地址和目的地址。最初所有的IP地址都是32位数字构成,由4个8位的二进制数组成,每8位之间用圆点隔开,如:192.168.1.1,这种类型的地址通过IPv4指定。
而现在有一种新的地址模式称为IPv6,IPv6使用128位数字表示一个地址,分为8个16位块。尽管IPv6比IPv4有很多优势,但是由于习惯的问题,很多设备还是采用IPv4。不过Java语言同时指出IPv4和IPv6。
在IPv4地址模式中IP地址分为A、B、C、D和E等5类。
* A类地址用于大型网络,地址范围:1.0.0.1~126.155.255.254。
* B类地址用于中型网络,地址范围:128.0.0.1~191.255.255.254。
* C类地址用于小规模网络,192.0.0.1~223.255.255.254。
* D类地址用于多目的地信息的传输和作为备用。
* E类地址保留仅作实验和开发用。
另外,有时还会用到一个特殊的IP地址127.0.0.1,127.0.0.1称为回送地址,指本机。主要用于网络软件测试以及本地机进程间通信,使用回送地址发送数据,不进行任何网络传输,只在本机进程间通信。
### 24.1.4 端口
一个IP地址标识这一台计算机,每一台计算机又有很多网络通信程序在运行,提供网络服务或进行通信,这就需要不同的端口进行通信。如果把IP地址比作电话号码,那么端口就是分机号码,进行网络通信时不仅要指定IP地址,还要指定端口号。
TCP/IP系统中的端口号是一个16位的数字,它的范围是0~65535。小于1024的端口号保留给预定义的服务,如HTTP是80,FTP是21,Telnet是23,Email是25等,除非要和那些服务进行通信,否则不应该使用小于1024的端口。
## 24.2 TCP Socket低层次网络编程
TCP/IP协议的传输层有两种传输协议:TCP(传输控制协议)和 UDP(用户数据报协议)。TCP是面向连接的可靠数据传输协议。TCP就像好比电话,电话接通后双方才能通话,在挂断电话之前,电话一直占线。TCP连接一旦建立起来,一直占用,直到关闭连接。另外,TCP为了保证数据的正确性,会重发一切没有收到的数据,还会对进行数据内容进行验证,并保证数据传输的正确顺序。因此TCP协议对系统资源的要求较多。
基于TCP Socket编程很有代表性,先介绍TCP Socket编程。
### 24.2.1 TCP Socket通信概述
Socket是网络上的两个程序,通过一个双向的通信连接,实现数据的交换。这个双向链路的一端称为一个Socket。Socket通常用来实现客户端和服务端的连接。Socket是TCP/IP协议的一个十分流行的编程接口,一个Socket由一个IP地址和一个端口号唯一确定,一旦建立连接Socket还会包含本机和远程主机的IP地址和远端口号,如图24-3所示,Socket是成对出现的。
![](https://box.kancloud.cn/842255068c15a53b11812d0790d4d7b5_935x346.png)
### 24.2.2 TCP Socket通信过程
使用Socket进行C/S结构编程,通信过程如图24-4所示。
![](https://box.kancloud.cn/f034b7d87bf7ccc53a9b49d88aa3d69a_796x743.png)
![](https://box.kancloud.cn/1a1ac48211c6b45714eabc04b6ee524c_906x715.png)
**图24-4 TCP Socket通信过程**
服务器端监听某个端口是否有连接请求,服务器端程序处于阻塞状态,直到客户端向服务器端发出连接请求,服务器端接收客户端请求,服务器会响应请求,处理请求,然后将结果应答给客户端,这样就会建立连接。一旦连接建立起来,通过Socket可以获得输入输出流对象。借助于输入输出流对象就可以实现服务器与客户端的通信,最后不要忘记关闭Socket和释放一些资源(包括:关闭输入输出流)。
### 24.2.3 Socket类
java.net包为TCP Socket编程提供了两个核心类:Socket和ServerSocket,分别用来表示双向连接的客户端和服务器端。
**本节先介绍一下Socket类,Socket常用的构造方法有:**
* Socket(InetAddress address, int port) :创建Socket对象,并指定远程主机IP地址和端口号。
* Socket(InetAddress address, int port, InetAddress localAddr, int localPort):创建Socket对象,并指定远程主机IP地址和端口号,以及本机的IP地址(localAddr)和端口号(localPort)。
* Socket(String host, int port):创建Socket对象,并指定远程主机名和端口号,IP地址为null,null表示回送地址,即127.0.0.1。
* Socket(String host, int port, InetAddress localAddr, int localPort):创建Socket对象,并指定远程主机和端口号,以及本机的IP地址(localAddr)和端口号(localPort)。host主机名,IP地址为null,null表示回送地址,即127.0.0.1。
**Socket其他的常用方法有:**
* InputStream getInputStream():通过此Socket返回输入流对象。
* OutputStream getOutputStream():通过此Socket返回输出流对象。
* int getPort():返回Socket连接到的远程端口。
* int getLocalPort():返回Socket绑定到的本地端口。
* InetAddress getInetAddress():返回Socket连接的地址。
* InetAddress getLocalAddress():返回Socket绑定的本地地址。
* boolean isClosed():返回Socket是否处于关闭状态。
* boolean isConnected():返回Socket是否处于连接状态。
* void close():关闭Socket。
**注意** Socket与流类似所占用的资源,不能通过JVM的垃圾收集器回收,需要程序员释放。一种方法是可以在finally代码块调用close()方法关闭Socket,释放流所占用的资源。另一种方法通过自动资源管理技术释放资源,Socket和ServerSocket都实现了AutoCloseable接口。
### 24.2.4 ServerSocket类
**ServerSocket类常用的构造方法有:**
* ServerSocket(int port, int maxQueue):创建绑定到特定端口的服务器Socket。maxQueue设置连接的请求最大队列长度,如果队列满时,则拒绝该连接。默认值是50。
* ServerSocket(int port):创建绑定到特定端口的服务器Socket。最大队列长度是50。
**ServerSocket其他的常用方法有:**
* InputStream getInputStream():通过此Socket返回输入流对象。
* OutputStream getOutputStream():通过此Socket返回输出流对象。
* boolean isClosed():返回Socket是否处于关闭状态。
* Socket accept():侦听并接收到Socket的连接。此方法在建立连接之前一直阻塞。
* void close():关闭Socket。
ServerSocket类本身不能直接获得I/O流对象,而是通过accept()方法返回Socket对象,通过Socket对象取得I/O流对象,进行网络通信。此外,ServerSocket也实现了AutoCloseable接口,通过自动资源管理技术关闭ServerSocket。
### 24.2.5 案例:文件上传工具
基于TCP Socket编程比较复杂,先从一个简单的文件上传工具案例介绍TCP Socket编程基本流程。上传过程是一个单向Socket通信过程,如图24-5所示
* 客户端通过文件输入流读取文件,然后从Socket获得输出流写入数据,写入数据完成上传成功,客户端任务完成。
* 服务器端从Socket获得输入流,然后写入文件输出流,写入数据完成上传成功,服务器端任务完成。
![](https://box.kancloud.cn/f7374372561b3ff03ddd92a095c2471e_923x343.png)
**下面看看案例服务器端UploadServer代码如下:**
```
~~~
//UploadServer.java文件
package com.a51work6;
… …
public class UploadServer {
public static void main(String[] args) {
System.out.println("服务器端运行...");
try ( // 创建一个ServerSocket监听8080端口的客户端请求
ServerSocket server = new ServerSocket(8080); ①
// 使用accept()阻塞当前线程,等待客户端请求
Socket socket = server.accept(); ②
// 由Socket获得输入流,并创建缓冲输入流
BufferedInputStream in = new BufferedInputStream(socket.getInputStream()); ③
// 由文件输出流创建缓冲输出流
FileOutputStream fiOut = new FileOutputStream("./TestDir/subDir/coco2dxcplus.jpg")
BufferedOutputStream out = new BufferedOutputStream(fiOut );
) {④
// 准备一个缓冲区
byte[] buffer = new byte[1024];
// 首次从Socket读取数据
int len = in.read(buffer);
while (len != -1) {
// 写入数据到文件
out.write(buffer, 0, len);
// 再次从Socket读取数据
len = in.read(buffer);
}
System.out.println("接收完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
~~~
```
上述代码第①行创建ServerSocket对象监听本机的8080端口,这是当前线程还没有阻塞,调用代码第②行的server.accept()才会阻塞当前线程,等待客户端请求。
> **提示** 由于当前线程是主线程,所以server.accept()会阻塞主线程,阻塞主线程是不明智的,如果是在一个图形界面的应用程序,阻塞主线程会导致无法进行任何的界面操作,就是常见的“卡”现象,所以最好是把server.accept()语句放到子线程中。
代码第③行是从socket对象中获得输入流对象,代码第④行是文件输出流。下面输入输出代码读者可以参考第22章,这里不再赘述。
**案例客户端UploadClient代码如下:**
```
~~~
//UploadClient.java文件
package com.a51work6;
… …
public class UploadClient {
public static void main(String[] args) {
System.out.println("客户端运行...");
try ( // 向本机的8080端口发出请求
Socket socket = new Socket("127.0.0.1", 8080); ①
// 由Socket获得输出流,并创建缓冲输出流
BufferedOutputStream out = new BufferedOutputStream(socket.getOutputStream());②
// 创建文件输入流
FileInputStream fin = new FileInputStream("./TestDir/coco2dxcplus.jpg");
// 由文件输入流创建缓冲输入流
BufferedInputStream in = new BufferedInputStream(fin)) {
// 准备一个缓冲区
byte[] buffer = new byte[1024];
// 首次读取文件
int len = in.read(buffer);
while (len != -1) {
// 数据写入Socket
out.write(buffer, 0, len);
// 再次读取文件
len = in.read(buffer);
}
System.out.println("上传成功!");
} catch (ConnectException e) { ③
System.out.println("服务器未启动!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
~~~
```
上述代码第①行创建Socket,指定远程主机的IP地址和端口号。代码第②行是从socket对象获得输出流。代码第③行是捕获ConnectException异常,这个异常引起的原因是在代码第①行向服务器发出请求时,服务器拒绝了客户端请求,这有两种可能性:一是服务器没有启动,服务器的8080端口没有打开;二是服务器请求队列已满(默认是50个)。
**提示** 案例测试时,先运行服务器,再运行客户端。测试Socket程序最好打开两个命令行窗口,通过java指令分别运行服务器程序和客户端程序,如图24-6和24-7所示,需要注意当前运行的路径是Eclipse工程根目录,需要指定类路径,命令的-cp .;./bin就是指定类路径,包括两个当前路径:其中点(.)表示当前路径,./bin表示bin目录,也可以写成.\\bin。为什么要指定bin目录呢?是因为编译之后的字节码文件放在此目录中。另外,如果想在Eclipse中查看多个控制台信息,如图24-8所示,在控制台上面的工具栏中,单击“选择控制台”按钮实现切换。
![](https://box.kancloud.cn/16c80c4359f7f43539ab433b49c588ff_969x496.png)
![](https://box.kancloud.cn/ef08a80422036a2ca292bbd73915863f_933x499.png)
![](https://box.kancloud.cn/3001abda471b9b371fd161ed9162358e_954x323.png)
### 24.2.6 案例:聊天工具
第24.2.5节介绍的案例只是单向传输的Socket,Socket可以双向数据传输,但是这就有些复杂了,比较有代表性的案例就是聊天工具。
如图24-9所示是基于TCP Socket聊天工具案例,其中的标准输入是键盘,标准输出是显示器的控制台。首先客户端通过键盘输入字符串,通过标准输入流读取字符串,然后通过Socket获得输出流,将字符串写入输出流。接着服务器通过Socket获得输入流,从输入流中读取来自客户端发送过来的字符串,然后通过标准输出流输出到显示器的控制台。服务器向客户端字符串过程类似。
![](https://box.kancloud.cn/dbf9d4f6ef590eb5972f5d07a63db5b7_942x388.png)
**案例服务器端ChatServer代码如下**