@king
2015-01-13T20:33:17.000000Z
字数 29369
阅读 2656
Java
按规模大小和延伸范围分:局域网LAN,城域网MAN,广域网WAN
按拓扑结构分:星型网络、总线网络、环线网络、树型网络、星型环线网络等
按网络的传输介质分:双绞线网、同轴电缆网、光纤网、卫星网
通信协议负责对传输速率、传输代码、代码结构、传输控制步骤、出错控制等制定处理标准。通常由三部分组成:
- 语义部分,用于决定双方对话的类型
- 语法部分,用于决定双方对话的格式
- 变换规则,用于决定通信双方的应答关系
开放系统互连参考模型 OSI(Open System Interconnection):
应用层
表示层
会话层
传输层
网络层
数据链路层
物理层
IP(Internet Protocol)协议---->互联网协议
TCP(Transmission Control Protocol)协议---->传输控制协议。
统称TCP/IP协议,将网络分为四层模型:
应用层
传输层
网络层
物理+数据链路层
IP地址ABCDE五类。
- A类:10.10.0.0~10.255.255.255
- B类:172.16.0.0~172.31.255.255
- C类:192.168.0.0~192.168.255.255
端口是一个16位(2个字节)的整数,用于表示数据交给哪个通信程序处理。端口就是应用程序与外界交流的出入口,它是一种抽象的软件结构,包括一些数据结构和I/O缓冲区。不同应用程序处理不同商品的数据,同一台机器上不能有两个程序使用同一个端口,端口号可以从0到65535,通常分为如下三类:
- 公认端口Well Known Ports:从0到1023,它们紧密绑定Binding一些特定的服务
- 注册端口Registered Ports:从1024到49151,它们松散地绑定一些服务,应用程序通常应该使用这个范围内的端口
- 动态和/或私有端口Dynamic and/or Private Ports:从49152到65535,这些端口是应用程序使用的动态端口,应用程序一般不会主动使用这些端口
java.net包下的URL和URLConnection类提供了以方式访问Web服务的可能,而URLDecoder和URLEncoder则提供了普通字符串和application/x-www-form-urlencoded MIME字符串相互转换的静态方法
Java 提供了 InetAddress 类来代表IP地址, InetAddress 下还有两个子类: Inet4Address、 Inet6Address,分别代表IPv4和IPv6地址。
InetAddress类没有提供构造器,而是提供了如下两个静态方法来获取InetAddress实例
- getByName(String host):根据主机获取对应的InetAddress对象
- getByAddress(byte[] addr):根据原始 IP 地址获取对应的 InetAddress 对象。
InetAddress还提供如下三个方法来获取InetAddress实例对应的IP地址和主机名。
- String getCanonicalHostName():获取此 IP 地址的完全限定域名。
- String getHostAddress():返回 IP 地址字符串(以字符串形式)。
- String getHostName():获取此 IP 地址的主机名。
除此之外,InetAddress类还提供了一个getLocalHost()方法来获取本机IP地址对应的InetAddress实例。
isReachable()方法,用于测试是否可以到达该地址。
import java.net.*;
public class Test{
public static void main(String[] args)throws Exception{
InetAddress ip = InetAddress.getByName("www.100ppi.com");
System.out.println("100ppi是否可达:" + ip.isReachable(2000));
//获取该InetAddress实例的IP字符串
System.out.println(ip.getHostAddress());
InetAddress local = InetAddress.getByAddress(new byte[]{127,0,0,1});
System.out.println("100ppi是否可达:" + ip.isReachable(2000));
System.out.println(local.getCanonicalHostName());
}
}
URLDecoder和URLEncoder则提供了普通字符串和application/x-www-form-urlencoded MIME字符串的相互转换。
当URl地址里包含非西欧字符的字符串时,系统会将这些非西欧字符串转换成特殊字符串,编程过程中可能涉及普通字符串和这种特殊字符串的相关转换,这就需要使用URLDecoder和URLEncoder类。
String keyWord = URLDecoder.decode("%E7%A5%9E", "GBK");
System.out.println(keyWord);
String urlStr = URLEncoder.encode("神一样的程序", "GBK");
System.out.println(urlStr);
下面引用API:
对 String 编码时,使用以下规则:
- 字母数字字符 "a" 到 "z"、"A" 到 "Z" 和 "0" 到 "9" 保持不变。
- 特殊字符 "."、"-"、"*" 和 "_" 保持不变。
- 空格字符 " " 转换为一个加号 "+"。
- 所有其他字符都是不安全的,因此首先使用一些编码机制将它们转换为一个或多个字节。然后每个字节用一个包含 3 个字符的字符串 "%xy" 表示,其中 xy 为该字节的两位十六进制表示形式。推荐的编码机制是 UTF-8。但是,出于兼容性考虑,如果未指定一种编码,则使用相应平台的默认编码。
UTF-8 每个汉字3个字节, GBK 每个汉字2个字节。
经测试,百度搜索的网址使用的是UTF-8编码。
URL(Uniform Resource Locator)对象代表统一资源定位器,它是指向互联网资源的指针。资源可以是简单的文件或目录,也可以是更复杂对象的引用,例如对数据库或搜索引擎的查询。
通常 URL 可以由协议名、主机、端口和资源组成: protocol://host:port/resourceName
JDK 中还提供了一个URI(Uniform Resource Identifiers)类,其实例代表一个统一资源标识符,Java的 URI 唯一作用就是解析。与此对应的是,URL 则包含一个可打开到达该资源的输入流,我们可以将 URL 理解成 URI 的特例
URL的常用方法,详见API。
如下程序实现了一个多线程下载工具类:
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
public class DownUtil {
//定义下载资源的路径
private String path;
//指定所下载的文件的保存位置
private String targetFile;
//定义需要使用多少个线程下载资源
private int threadNum;
//定义下载的线程对象
private DownThread[] threads;
//定义下载的文件的总大小
private int fileSize;
public DownUtil(String path, String targetFile, int threadNum){
this.path = path;
this.threadNum = threadNum;
//初始化thread数组
threads = new DownThread[threadNum];
this.targetFile = targetFile;
}
public void download() throws Exception{
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5 * 1000); //超时值
conn.setRequestMethod("GET"); //设置 URL 请求的方法
conn.setRequestProperty(
"Accept",
"image/gif, image/jpeg, image/pjpeg, image/pjpeg, "
+ "application/x-shockwave-flash, application/xaml+xml, "
+ "application/vnd.ms-xpsdocument, application/x-ms-xbap, "
+ "application/vnd.ms-powerpoint, application/msword, */*");
conn.setRequestProperty("Accept-Language", "zh-CN");
conn.setRequestProperty("Charset", "UTF-8");
conn.setRequestProperty("Connection", "Keep-Alive");
//得到文件大小
fileSize = conn.getContentLength();
conn.disconnect();
int currentPartSize = fileSize / threadNum + 1;
RandomAccessFile file = new RandomAccessFile(targetFile, "rw");
//设置本地文件大小
file.setLength(fileSize);
file.close();
for(int i = 0; i < threadNum; i++){
//计算每个线程下载的开始位置
int startPos = i * currentPartSize;
//每个线程使用一个RandomAccessFile进行下载
RandomAccessFile currentPart = new RandomAccessFile (targetFile, "rw");
//定位该线程的下载位置
currentPart.seek(startPos);
//创建下载线程
threads[i] = new DownThread(startPos, currentPartSize, currentPart);
//启动下载线程
threads[i].start();
}
}
//获取下载的完成百分比
public double getCompleteRate(){
//统计多个线程已经下载的总大小
int sumSize = 0;
for(int i = 0; i < threadNum; i++){
sumSize += threads[i].length;
}
//返回已经完成的百分比
return sumSize * 1.0 / fileSize;
}
//下载用的线程
private class DownThread extends Thread{
//当前线程的下载位置
private int startPos;
//定义当前线程负责下载的文件大小
private int currentPartSize;
//当前线程需要下载的文件块
private RandomAccessFile currentPart;
//定义该线程已经下载的字节数
public int length;
public DownThread(int startPos, int currentPartSize, RandomAccessFile currentPart){
this.startPos = startPos;
this.currentPartSize = currentPartSize;
this.currentPart = currentPart;
}
public void run(){
try{
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5 * 1000); //超时值
conn.setRequestMethod("GET"); //设置 URL 请求的方法
conn.setRequestProperty(
"Accept",
"image/gif, image/jpeg, image/pjpeg, image/pjpeg, "
+ "application/x-shockwave-flash, application/xaml+xml, "
+ "application/vnd.ms-xpsdocument, application/x-ms-xbap, "
+ "application/vnd.ms-powerpoint, application/msword, */*");
conn.setRequestProperty("Accept-Language", "zh-CN");
conn.setRequestProperty("Charset", "UTF-8");
InputStream inStream = conn.getInputStream();
//跳过startPos个字节,表明该线程只下载自己负责的那部分文件
inStream.skip(this.startPos);
byte[] buffer = new byte[1024];
int hasRead = 0;
//读取网络数据,并写入本地文件
while (length < currentPartSize && (hasRead = inStream.read(buffer)) != -1){
currentPart.write(buffer, 0, hasRead);
//累计该线程下载的总大小
length += hasRead;
}
currentPart.close();
inStream.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
程序中DownUtils类中的download()方法负责按如下步骤来实现多线程下载:
- 创建URL对象
- 获取指定URl对象所指向资源的大小(getContentLength())
- 在本地磁盘上创建一个与网络资源具有相同大小的空文件
- 计算每个线程应该下载网络资源的哪个部分
- 依次创建、启动多个线程来下载网络资源的指定部分。
如果需要实现断点下载,则需要额外增加一个配置文件,分别记录每个线程已经下载到哪个字节,当网络断开后再次开始下载时,每个线程根据配置文件里记录的位置向后下载。
有了上面的DownUtil工具类之后,接下来就可以在主程序中调用该工具类的down()方法执行下载,如下程序所示
public class MultiThreadDown{
public static void main(String[] args) throws Exception{
//初始化DownUtil对象
final DownUtil downUtil = new DownUtil("下载地址", "保存地址", 4);
//开始下载
downUtil.download();
new Thread(){
public void run(){
while(downUtil.getCompleteRate() < 1){ //没下载完成
//每隔0.1秒查询一次任务的完成进度
//GUI程序中可根据该进度来绘制进度条
System.out.println("已完成:" + downUtil.getCompleteRate());
try{
Thread.sleep(1000); //一秒?
}catch (Exception e) {}
}
}
}.start();
}
}
上面的程序还用到URLConnection和HttpURLConnection对象,其中前者表示应用程序和URL之间的通信连接,后者表示应用程序与URL之间的HTTP连接。程序可以通过URLConnection实例向该URL发送请求、读取URL引用的资源。
通常创建一个和URl的连接,并发送请求、此URL引用的资源需要如下几个步骤:
<1> 通过调用URL对象的openConnection()方法来创建URLConnection对象。
<2>设置URLConnection的参数和普通请求属性
<3>如果只是发送GET方式请求,则使用connect()方法建立和远程资源之间的实际连接既可;如果需要发送POST方式的请求,则需要获取URLConnection实例对应的输出流来发送请求参数
<4>远程资源变为可用,程序可以访问远程资源的头字段或输入流读取远程资源的数据。
详见API
如果即要使用输入流读取URLConnection响应的内容,又要使用输出流发送请求参数,则一定要先使用输出流,再使用输入流
下面程序示范了如何向Web站点发送GET请求、POST请求,请从Web站点取得响应:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.URL;
import java.net.URLConnection;
import java.util.List;
import java.util.Map;
public class GetPostTest {
// 向指定URL发送GET方式的请求
public static String sendGet(String url, String param){
String result = "";
String urlName = url + "?" + param;
try{
URL realUrl = new URL(urlName);
//打开和URL之间的连接
URLConnection conn = realUrl.openConnection();
//设置通用的请求属性
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)");
//建立实际的连接
conn.connect();
//获取所有响应头字段
Map<String, List<String>> map = conn.getHeaderFields();
//遍历所有的响应头字段
for(String key : map.keySet()){
System.out.println(key + "--->" + map.get(key));
}
try(
// 定义BufferedReader输入流来读取URL的响应
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"))){
String line;
while ((line = in.readLine()) != null){
result += "\n" + line;
}
}
}catch(Exception e){
System.out.println("发送GET请求出现异常!" + e);
e.printStackTrace();
}
return result;
}
//向指定URL发送POST方式的请求
public static String sendPost(String url, String param){
String result = "";
try{
URL realUrl = new URL(url);
// 打开和URL之间的连接
URLConnection conn = realUrl.openConnection();
//设置通用的请求属性
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible); MSIE 6.0; Windows NT 5.1; SV1)");
//发送Post请求必须设置如下两行
conn.setDoOutput(true);
conn.setDoInput(true);
try(
//获取URLConnection对象对应的输出流
PrintWriter out = new PrintWriter(conn.getOutputStream())){
//发送请求参数
out.println(param);
out.flush();
}
try(
//定义BufferedReader来读取URL的响应
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"))){
String line;
while((line = in.readLine()) != null){
result += "\n" + line;
}
}
}catch (Exception e){
System.out.println("发送POST请求出现异常" + e);
e.printStackTrace();
}
return result;
}
//提供主方法,测试发送GET请求和POST请求
public static void main(String args[]){
//发送GET请求
String s = GetPostTest.sendGet("http://blog.sina.com.cn/king881204", null);
System.out.println(s);
// 发送POST请求
// 相当于提交Web应用中的登录表单页
String s1 = GetPostTest.sendPost("http://localhost:8888/abc/login.jsp", "name=crazyit.org&pas=leegang");
System.out.println(s1);
}
}
上面程序中发送GET请求时只需将请求参数放在URL字符串之后,以?隔开,程序直接调用URLConnection对象的connect()方法即可;如果程序要发送POST请求,则需要先设置doIn和doOut两个请求头字段的值,再使用URLConnection对应的输出流来发送请求参数。
以下代码向 baidu 发一次请求,并获取结果进行显示。例子演示用到了SocketChannel和charset 的使用。
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.net.InetSocketAddress;
import java.io.IOException;
public class Test {
private Charset charset = Charset.forName("GBK");// 创建GBK字符集
private SocketChannel channel;
public void readHTMLContent() {
try {
InetSocketAddress socketAddress = new InetSocketAddress(
"www.baidu.com", 80);
//step1:打开连接
channel = SocketChannel.open(socketAddress);
//step2:发送请求,使用GBK编码
channel.write(charset.encode("GET " + "/ HTTP/1.1" + "\r\n\r\n"));
//step3:读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);// 创建1024字节的缓冲
while (channel.read(buffer) != -1) {
buffer.flip();// flip方法在读缓冲区字节操作之前调用。
System.out.println(charset.decode(buffer));
// 使用Charset.decode方法将字节转换为字符串
buffer.clear();// 清空缓冲
}
} catch (IOException e) {
System.err.println(e.toString());
} finally {
if (channel != null) {
try {
channel.close();
} catch (IOException e) {
}
}
}
}
public static void main(String[] args) {
new Test().readHTMLContent();
}
}
TCP/IP通信协议是一种可靠的网络协议,它在通信的两端各建立一个Socket,从而在通信的两端之间形成网络虚拟链路。一旦建立了虚拟的网络链路,两端的程序就可以通过虚拟链路进行通信。
Java使用Socket对象来代表两端的通信端口,并通过Socket产生IO流来进行网络通信
IP协议全称Internet Protocol。IP协议只保证计算机能发送和接收分组数据。IP协议负责将消息从一个主机传送到另一个主机,消息在传送的过程中被分割成一个个的小包。
为解决传输过程中可能出现的问题,连上Internet的计算机还需要安装TCP协议来提供可靠并且无差错的通信服务。TCP协议会让两台需要连接的计算机建立一个用于发送和接收数据的虚拟链路。
TCP协议负责收集这些信息包,并按适当的次序放好发送,接收端收到后再将其正确地还原。TCP协议保证了数据包在传送中准确无误。TCP协议使用重发机制:当一个通信实体发送一个消息给另一个通信实体后,需要收到另一个通信实体的确认信息,如果没有收到,则会再次重发刚才发送的信息。
在两个通信实体没有建立虚拟链路之前,必须有一个先做出“主动姿态”,主动接收来自其他通信实体的连接请求。
Java中能接收其他通信实体连接请求的类是ServerSocket, ServerSocket对象用于监听来自客户端的Socket连接,如果没有连接,它将一直处于等待状态。
ServerSocket包含一个监听来自客户端连接请求的方法:
- Socket accept():如果接收到一个客户端Socket的连接请求,该方法将返回一个与客户端Socket对应的Socket;否则该方法将一直处于等待状态,线程也被阻塞
当ServerSocket使用完毕后,应使用close()方法来关闭该ServerSocket。通常情况下,服务器不应该只接收一个客户端请求,所以Java程序通常通过循环不断地调用ServerSocket的accept()方法
//创建一个ServerSocket
ServerSocket ss = new ServerSocket(30000);
while(true){
//每当接收到客户端Socket请求时,服务器端也对应产生一个Socket
Socket s = ss.accept();
//下面就可以使用Socket进行通信了
.....
}
上面程序中创建ServerSocket没有指定IP地址,则它会绑定到本机默认的IP。程序使用30000作为该ServerSocket的端口号,通常推荐使用1024以上的端口,主要是为了避免与其他应用程序的通用端口冲突。
客户端通常可以使用Socket的构造器来连接到指定服务器。如:
Socket s = new Socket("127.0.0.1", 30000);
该代码会连接到指定服务器,让服务器端的ServerSocket的accept()方法向下执行,于是服务器端和客户端就产生一对互相连接的Socket。
127.0.0.1 是一个特殊的地址,它总是代表本机的IP地址。
当客户端、服务器端产生了对应的Socket之后,就无须再区分服务器端、客户端。
下面的服务器端程序非常简单,它仅建立ServerSocket监听,并使用Socket获取输出流输出
import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException{
//创建一个ServerSocket,用于监听客户端的Socket连接请求
ServerSocket ss = new ServerSocket(30000);
//采用循环不断接收来自客户端的请求
while(true){
Socket s = ss.accept();
PrintStream ps = new PrintStream(s.getOutputStream());
ps.println("您好,您收到了服务器的新祝福!");
ps.close();
s.close();
}
}
}
下面是客户端程序接收消息
package king.net;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
public class Client {
public static void main(String[] args)throws IOException{
Socket socket = new Socket("127.0.0.1", 30000);
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = br.readLine();
System.out.println("来自服务器的数据:" + line);
br.close();
socket.close();
}
}
也可以设置超时时长,超时则抛出SocketTimeoutException异常
Socket s = new Socket("127.0.0.1", 30000);
s.setSoTimeout(10000);
try{
Scanner scan = new Scanner(s.getInputStream());
String line = scan.nextLine();
...
}catch(SocketTimeoutException e){
//处理异常
//...
}
使用传统IO流读取数据时,线程会阻塞。所以服务器端应该为每个Socket单独启动一个线程,每个线程负责与一个客户端进行通信。客户端也一样。
下面是服务器端的实现代码
package king.net;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
public class MyServer {
//定义保存所有Socket的ArrayList
public static ArrayList<Socket> socketList = new ArrayList<Socket>();
public static void main(String[] args)throws IOException{
ServerSocket ss = new ServerSocket(30000);
while(true){
//此行代码会阻塞,将一直等待别人的连接
Socket s = ss.accept();
socketList.add(s);
//每当客户端连接后启动一个ServerThread线程为该客户端服务
new Thread(new ServerThread(s)).start();
}
}
}
服务器端线程类的代码如下:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
//负责处理每个线程通信的线程类
public class ServerThread implements Runnable {
//定义当前线程所处理的
Socket s = null;
//该线程所处理的Socket对应的输入流
BufferedReader br = null;
public ServerThread(Socket s) throws IOException{
this.s = s;
//初始化该Socket对应的输入流
br = new BufferedReader(new InputStreamReader(s.getInputStream()));
}
@Override
public void run() {
try{
String content = null;
//采用循环不断地从Socket中读取客户端发送过来的数据
while((content = readFromClient()) != null){
//将读到的内容向每个Socket发送一次
for(Socket s : MyServer.socketList){
PrintStream ps = new PrintStream(s.getOutputStream());
ps.println(content);
}
}
}catch(IOException e){
e.printStackTrace();
}
}
//定义读取客户端数据的方法
private String readFromClient(){
try{
return br.readLine(); //这里会阻塞等待对方输入消息
}catch(IOException e){
//如果捕获到异常,则表明该Socket对应的客户端已经关闭
//删除该Socket
MyServer.socketList.remove(s);
}
return null;
}
}
每个客户端应该包含两个线程,一个负责读取用户的键盘输入,并将用户输入的数据写入Socket对应的输出流中;一个负责读取Socket对应输入流中的数据(从服务器端发送过来的数据),并处理它们。
下面示例中负责读取用户键盘输入的线程由主线程负责
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
public class MyClient {
public static void main(String[] args) throws Exception{
Socket s = new Socket("127.0.0.1", 30000);
//客户端启动ClientThread线程不断地读取来自服务器的数据
new Thread(new ClientThread(s)).start();
PrintStream ps = new PrintStream(s.getOutputStream());
String line = null;
//读取键盘输入
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while((line = br.readLine()) != null){
//将用户的键盘输入内容写入Socket对应的输出流
ps.println(line);
}
}
}
ClientThread线程负责读取Socket输入流中的内容
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
public class ClientThread implements Runnable {
private Socket s;
BufferedReader br = null;
public ClientThread(Socket s)throws IOException{
this.s = s;
br = new BufferedReader(new InputStreamReader(s.getInputStream()));
}
@Override
public void run() {
try{
String content = null;
while((content = br.readLine()) != null){
System.out.println(content);
}
}catch(Exception e){
e.printStackTrace();
}
}
}
上面粗略地实现了一个C/S结构聊天室的功能。
如果直接关闭输出流,则对应的Socket也会被关闭,程序无法从该Socket的输入流中读取数据了。因此Socket提供了两个半关闭的方法shutdownInput()和shutdownOutput();
调用其中一个方法后,该Socket处于半关闭状态。可以用isInputShutdown()或者isOutputShutdown()检查其状态。
即使同一个Socket实例先后调用shutdownInput()、shutdownOutput()方法,该Socket实例依然没有被关闭
前面介绍的网络通信程序是基于阻塞式API的,必须用服务器必须为每个客户端提供一个独立的线程进行处理。
JDK 1.4 开始的 NIO API 可以开发高性能的网络服务器,使用一个或有限的几个线程可以同时处理连接到服务器端的所有客户端。
NIO 为非阻塞式 Socket 通信提供了如下几个特殊类:
- Selector:它是SelectableChannel对象的多路利用器,所有希望采用非阻塞方式进行通信的Channel都应该注册到Selector对象。可以通过调用此类的open()静态方法来创建Selector实例,该方法将使用默认的Selector来返回新的Selector。
Selector可以同时监控多个SelectableChannel的IO状态,是非阻塞 IO 的核心。一个Selector实例有3个SelectionKey集合
P796
- 所有的SelectionKey集合:代表了注册在该Selector上的Channel,这个集合可以通过keys()方法返回
- 被选择的SelectionKey集合:代表了所有可通过select()方法获取的、需要进行IO处理的Channel,这个集合可以通过selectedKeys()返回。
- 被取消的SelectionKey集合:代表了所有被取消注册关系的Channel,在下一次执行select()方法时,这些Channel对应的SelectionKey会被彻底删除,程序通过无须直接访问该集合
应用程序可以调用SelectableChannel的register()方法将其注册到指定Selector上,当该Selector上的某些SelectableChannel上有需要处理的IO操作时,程序可以调用Selector实例的select()方法获取它们的数量,并可以通过selectedKeys9)方法返回把它对应的SelectionKey集合——通过该集合就可以获取所有需要进行IO处理的SelectableChannel集。
SelectableChannel对象支持阻塞和非阻塞两种模式(所有的Channel默认都是阻塞模式),必须使用非阻塞模式才可以利用非阻塞IO操作。configureBlocking(false);
不同的SelectableChannel所支持的操作不一样。用 validOps()方法返回它所支持的所有操作。
服务器上所有Channel都需要向Selector注册,该Selector负责监视这些Socket的IO状态,将其中任意一个或多个Channel具有可用IO操作时,该Selector的select()方法将会返回其数量值,并提供了selectedKeys()方法来返回这些Channel对应的SelectionKey集合。
服务器端需要使用ServerSocketChannel来监听客户端的连接请求,程序必须先调用它的open()静态方法返回一个ServerSocketChannel实例,再使用它的bind()方法指定它在某个商品监听。
ServerSocketChannel server = ServerSocketChannel.open();
InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 30000);
erver.bind(isa);
同时要设置它的非阻塞模式,并将其注册到指定的Selector:
server.configureBlocking(false);
server.register(selector, SelectionKey.OP_ACCEPT); //ServerSocketChannel仅支持OP_ACCEPT操作
package king.net;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
public class NServer {
//用于检测所有Channel状态的Selector
private Selector selector = null;
static final int PORT = 30000;
//定义实现编码、解码的字符集对象
private Charset charset = Charset.forName("UTF-8");
public void init()throws IOException{
selector = Selector.open();
//通过open方法来打开一个未绑定的ServerSocketChannel实例
ServerSocketChannel server = ServerSocketChannel.open();
InetSocketAddress isa = new InetSocketAddress("127.0.0.1", PORT);
//将该ServerSocketChannel绑定到指定IP地址
server.bind(isa);
//设置ServerSocketChannel以非阻塞方式工作
server.configureBlocking(false);
//将server注册到指定的Selector对象
server.register(selector, SelectionKey.OP_ACCEPT);
while(selector.select() > 0){
//依次处理selector上每个已选择的SelectionKey
for(SelectionKey sk : selector.selectedKeys()){
//从selector上的已选择Key集中删除正在处理的SelectionKey
selector.selectedKeys().remove(sk);
//如果sk对应的Channel包含客户端的连接请求
if(sk.isAcceptable()){
//调用accept方法接受连接,产生服务器端的SocketChannel
SocketChannel sc = server.accept();
sc.configureBlocking(false);
//将该SocketChannel也注册到Selector
sc.register(selector, SelectionKey.OP_READ);
//将sk对应的Channel设置成准备接收其他请求
sk.interestOps(SelectionKey.OP_ACCEPT);
}
//如果sk对应的Channel有数据要读取
if (sk.isReadable()){
//获取该SelectionKey对应的Channel,该Channel中有可读的数据
SocketChannel sc = (SocketChannel)sk.channel();
//定义准备执行读取数据的ByteBuffer
ByteBuffer buff = ByteBuffer.allocate(1024);
String content = "";
//开始读取数据
try{
while(sc.read(buff) > 0){
buff.flip();
content += charset.decode(buff);
buff.clear();
}
//打印从该sk对应的Channel里读取到的数据
System.out.println("读取的数据:" + content);
//将sk对应的Channel设置成准备下一次读取
sk.interestOps(SelectionKey.OP_READ);
}catch(IOException e){ //有异常则取消注册
//从Selector中删除指定的SelectionKey
sk.cancel();
if(sk.channel() != null){
sk.channel().close();
}
}
//如果content的长度大于0,即聊天信息不为空,则给所有注册的Channel都广播此消息
if(content.length() > 0){
//遍历该Selector里注册的所有SelectionKey
for(SelectionKey key : selector.keys()){
//获取该key对应的Channel
Channel targetChannel = key.channel();
//如果该Channel是SocketChannel对象
if (targetChannel instanceof SocketChannel){
//将读取的内容写入该Channel中
SocketChannel dest = (SocketChannel)targetChannel;
dest.write(charset.encode(content));
}
}
}
}
}
}
}
public static void main(String[] args) throws IOException {
new NServer().init();
}
}
客户端需要两个线程,一个线程负责读取用户的键盘输入,并将输入的内容写入SocketChannel中;另一个线程则不断地查询Selector对象的select()方法的返回值,如果该方法的返回值大于0,就说明程序需要对相应的Channel执行IO处理。
package king.net;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Scanner;
public class NClient {
private Selector selector = null;
static final int PORT = 30000;
private Charset charset = Charset.forName("UTF-8");
private SocketChannel sc = null;
public void init() throws Exception{
selector = Selector.open();
InetSocketAddress isa = new InetSocketAddress("127.0.0.1", PORT);
sc = SocketChannel.open(isa); //创建连接到指定主机的SocketChannel
sc.configureBlocking(false); //非阻塞式
sc.register(selector, SelectionKey.OP_WRITE);
new ClientThread().start(); //读取服务器端数据
Scanner scan = new Scanner(System.in);
while(scan.hasNextLine()){
String line = scan.nextLine();
sc.write(charset.encode(line)); //输出到SocketChannel
}
}
private class ClientThread extends Thread{
public void run(){
try{
while(selector.select() > 0){
for(SelectionKey sk : selector.selectedKeys()){
selector.selectedKeys().remove(sk);
if (sk.isReadable()){
//使用NIO读取Channel中的数据
SocketChannel sc = (SocketChannel) sk.channel();
ByteBuffer buff = ByteBuffer.allocate(1024);
String content = "";
while(sc.read(buff) >0){
buff.flip();
content += charset.decode(buff);
buff.clear();
}
System.out.println("聊天信息:" + content);
//为下一次读取作准备
sk.interestOps(SelectionKey.OP_READ);
}
}
}
}catch(IOException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) throws Exception{
new NClient().init();
}
}
Java 7 的NIO.2提供了异步Channel支持,可以提供更高效的IO,这种基于民步Channel的IO机制也被称为异步IO(Asynchronous IO)。
阻塞IO、非阻塞IO是根据程序发出IO请求时会不会阻塞线程而划分的。
同步IO、异步IO的的区别在于完成实际的IO操作。如果实际的IO操作由操作系统完成,再将结果返回给应用程序,就是异步IO;如果实际的IO需要应用程序本身去执行,会阻塞线程,那就是同步IO。前面介绍的传统IO、基于Channel的非阻塞IO其实都是同步IO。
NIO.2提供了一系列以Asynchronous开头的Channel接口和类。其实AsynchronousSocketChannel、AsynchronousServerSocketChannel是支持TCP通信的异步Channel。
AsynchronousServerSocketChannel是一个负责监听的Channel,与ServerSocketChannel相似,创建可用的AsynchronousServerSocketChannel需要如下两步:
- 调用它的open()静态方法创建一个未监听商品的AsynchronousServerSocketChannel
- 调用AsynchronousServerSocketChannel的bind()方法指定该Channel在指定地址、指定端口监听。
AsynchronousServerSocketChannel的open()方法有两个版本:
- open():创建一个默认的AsynchronousServerSocketChannel
- open(AsynchronousChannelGroup group): 使用指定的AsynchronousChannelGroup来创建AsynchronousServerSocketChannel。
上面方法中的AsynchronousChannelGroup是异步Channel的分组管理器,它可以实现资源共享。创建AsynchronousChannelGroup时需要传入一个ExecutorService,也就是说,它会绑定一个线程池,该线程池负责两个任务:处理IO事件和触发CompletionHandler
AIO的 AsynchronousServerSocketChannel、 AsynchronousSocketChannel都允许使用线程池进行管理,因此创建AsynchronousSocketChannel时也可以传入AsynchronousChannelGroup对象进行分组管理
直接创建AsynchronousServerSocketChannel:
serverChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(PORT));
使用AsynchronousChannelGroup创建AsynchronousServerSocketChannel:
ExecutorService executor = Executors.newFixedThreadPool(80);
AsynchronousChannelGroup channelGroup = AsynchronousChannelGroup.withThreadPool(executor);
serverChannel = AsynchronousServerSocketChannel.open(channelGroup).bind(new InetSocketAddress(PORT));
AsynchronousServerSocketChannel创建成功之后,可以调用它的accept方法来接受客户端的连接,由于异步IO的实际IO操作是交给操作系统来完成的,因此程序并不清楚异步IO操作什么时候完成。为解决这个异步问题,AIO为accept方法提供了如下两个版本
- Future accept():接受客户端的请求。如果程序需要获得连接成功后返回的AsynchronousServerSocketChannel,则应该调用方法返回的Future对象的get()方法——但get()方法会阻塞线程
- void accept(A attachment, CompletionHandler handler):接受来自客户端的请求,连接成功或连接失败都会触发CompletionHandler对象里相应的方法
CompletionHandler是一个接口,定义了如下两个方法:
- completed(V result, A attachment):当IO操作完成时触发该方法。第一个参数代表IO操作所返回的对象,第二个参数代表发起IO操作时传入的附加参数。
- failed(Throwable exc, A attachment):当IO操作失败时触发该方法。第一个参数代表IO操作失败引发的异常或错误,第二个参数代表发起IO操作时传入的附加参数。
下面用最简单、最少的步骤来实现一个基于AsynchronousServerSocketChannel服务器端
package king.net;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.util.concurrent.Future;
public class SimpleAIOServer {
static final int PORT = 30000;
public static void main(String[] args) throws Exception{
//创建AsynchronousServerSocketChannel对象
AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();
//指定在地址、端口监听
serverChannel.bind(new InetSocketAddress(PORT));
while(true){
//采用循环接受来自客户端的连接
Future<AsynchronousSocketChannel> future = serverChannel.accept();
//获取连接完成后返回的AsynchronousSocketChannel
AsynchronousSocketChannel socketChannel = future.get();
socketChannel.write(ByteBuffer.wrap("Welcom!".getBytes("UTF-8"))).get();
}
serverChannel.close();
}
}
AsynchronousSocketChannel的用法也分为3步:
- 调用open()静态方法创建AsynchronousSocketChannel。同样可以指定一个Asyn作为分组管理器
- connect()连接
- read()、write()读写
后面三个方法同样都有两个版本:返回Future对象和需要传入CompletionHandler参数的版本。
下面先用返回Future对象的read()方法来读取服务器端响应数据
package king.net;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.charset.Charset;
public class SimpleAIOClient {
static final int PORT = 30000;
public static void main(String[] args)throws Exception {
ByteBuffer buff = ByteBuffer.allocate(1024);
Charset utf = Charset.forName("utf-8");
AsynchronousSocketChannel clientChannel = AsynchronousSocketChannel.open();
clientChannel.connect(new InetSocketAddress("127.0.0.1", PORT)).get();
buff.clear(); //准备写入
clientChannel.read(buff).get(); //调用Future返回值的get()方法来等待异步IO操作已经完成
buff.flip();
String content = utf.decode(buff).toString();
System.out.println("服务器信息:" + content);
}
}
使用线程池来管理异步Channel并使用CompletionHandler来监听异步IO操作的代码,详见《疯狂Java讲义》 P804
UDP协议是一种不可靠的协议,它在通信的两端各建立一个Socket,但中间没有虚拟链路。
Java提供了DatagramSocket对象作为基于UDP协议的Socket,使用DatagramPacket代表DatagramSocket发送、接收的数据报。
UDP协议是英文User Datagram Protocol 的缩写,即用户数据报协议。应用不如TCP广泛,但在网络游戏、视频会议等实时性很强的应用场景中,其快速性更有用。
UDP 协议是一种面向非连接的协议,即正式通信前不必先与对方建立连接,不管对方状态就直接发送。通信效率很高。适用于一次只传送少量数据,对可靠性要求不高的应用环境。
UDP协议直接位于IP之上,和TCP一样都属性传输层协议。 IP是网络层。
简单对比:
- TCP 协议: 可靠,传输大小无限制,但需要连接建立时间,差错控制开销大
- UDP 协议: 不可靠,差错控制开销较小,传输大小限制在64KB以下,不需要建立连接。
DatagramSocket不能产生IO流,它的唯一作用就是接收和发送数据报。 通过使用DatagramPacket来代表数据报。
用receive和send方法来收发数据报。DatagramSocket并不知道将该数据报发送到哪里,而是由DatagramPacket自身决定数据报的目的地。
当C/S程序使用UDP协议时,并没有显示的服务器端和客户端,通常固定IP地址、固定商品的DatagramSocket对象所在的程序被称为服务器,因为它可以主动接收客户端数据。
接收数据:
//创建一个接收数据的DatagramPacket对象
DatagramPacket packet = new DatagramPacket(buf, buf.length);
//接收数据报
socket.receive(packet);
发送数据:
// 创建一个发送数据的DatagramPacket对象
DatagramPacket packet = new DatagramPacket(buf, length, address, port);
//发送数据报
socket, send(packet);
由于UDP协议是面向非连接的,接收者并不知道每个数据报由谁发送过来,但程序可以调用DatagramPacket的如下3个方法来获取发送者的IP地址和端口: getAddress(), getPort(), getSocketAddress()。
SocketAddress实际上是一个IP地址和一个端口号。
下面程序使用DatagramSocket实现了S/C结构的网络通信,以下为服务器端:
package king.net;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UdpServer {
public static final int PORT = 30000;
private static final int DATA_LEN = 4096; //定义每个数据报的最大大小为4KB
byte[] inBuff = new byte[DATA_LEN];
private DatagramPacket inPacket = new DatagramPacket(inBuff, inBuff.length);
private DatagramPacket outPacket;
String[] books = { "test", "king"};
public void init() throws IOException{
DatagramSocket socket = new DatagramSocket(PORT);
for(int i = 0; i < 1000; i++){ //接收一千次
socket.receive(inPacket);
System.out.println(inBuff == inPacket.getData()); //判断两者是否同一个数组,用于验证其底层实现,前者传入构造器,后者为取出的数据
System.out.println(new String(inPacket.getData(), 0, inPacket.getLength()));
byte[] sendData = books[i%2].getBytes(); //从字符串中取出一个元素作为发送数据
outPacket = new DatagramPacket(sendData, sendData.length, inPacket.getSocketAddress());
socket.send(outPacket);
socket.close();
}
}
public static void main(String[] args) throws Exception{
new UdpServer().init();
}
}
以下为客户端:
package king.net;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
public class UdpClient {
public static final int DEST_PORT = 30000;
public static final String DEST_IP = "127.0.0.1";
private static final int DATA_LEN = 4096;
byte[] inBuff = new byte[DATA_LEN];
private DatagramPacket inPacket = new DatagramPacket(inBuff, inBuff.length);
private DatagramPacket outPacket = null;
public void init() throws IOException{
DatagramSocket socket = new DatagramSocket(); //使用随机端口
outPacket = new DatagramPacket(new byte[0], 0, InetAddress.getByName(DEST_IP), DEST_PORT); //初始化
Scanner scan = new Scanner(System.in);
while(scan.hasNextLine()){
byte[] buff = scan.nextLine().getBytes();
outPacket.setData(buff); //设置发送用的DatagramPacket中的字节数据
socket.send(outPacket);
socket.receive(inPacket);
System.out.println(new String(inPacket.getData(), 0, inPacket.getLength()));
}
socket.close();
}
public static void main(String[] args)throws IOException {
new UdpClient().init();
}
}
DatagramSocket只允许数据报发送给指定的目标地址, 而MulticastSocket可以将数据报以广播方式发送到多个客户端
使用多点广播需要让一个数据报标有一组目标主机地址,当数据报发出后,整个组的所有主机都能收到该数据报。IP多点广播实现了将单一信息发送到多个接收者的广播,其思想是设置一组特殊网络地址作为多点广播地址,每一个多点广播地址都被看做一个组,当客户端要求发送、接收广播信息时,加入到该组即可。
IP 为多点广播提供了这批特殊的IP地址,这些IP地址的范围是224.0.0.0至239.255.255.255.
当MulticastSocket把一个DatagramPacket发送到多点广播IP地址时,该数据报将被自动广播到加入该地址的所有MulticastSocket。MulticastSocket即可以将数据报发送到多点广播地址,也可以接收其他主机的广播信息。
MulticastSocket使用joinGroup()方法加入指定组,使用leaveGroup()方法脱离一个组。setInterface()强制MulticastSocket使用指定的网络接口;也可以用getInterface()查询MulticastSocket监听的网络接口。
创建仅用于发送数据报的MulticastSocket对象可以使用默认地址、随机端口。但如果创建接收用的MulticastSocket对象,则必须具有指定商品,否则发送方无法确定发送数据报的目标端口。
MulticastSocket收发数据报的方法与DatagramSocket完全一样,但多了一个setTimeToLive(int ttl)方法。用于设置数据报最多可以跨过多少个网络。当值为0时,指定数据报停留在本地主机,值为1时,本地局域网;32为本站点的网络;64为本地区;128为本大洲, 255为所有地方。默认为1。
示例见《疯狂Java讲义》 P812
局域网即时聊天工具思路:每个用户都启动两个Socket,一个MulticastSocket,一个Datagr,前者会周期性向多播地址发送在线信息,且所有用户的MulticastSocket都会加入其中。如果系统经过一段时间没有收到某个用户广播的在线信息,则从用户列表中删除该用户。除此之外,该MulticastSocket还用于向所有用户发送广播信息。
DatagramSocket用于发送私聊信息。
代码见《疯狂Java讲义》 P814
Java 5 开始,Java 在 java.net包下提供了Proxy和ProxySelector两个类。Proxy代表一个代理服务器,可以在打开URLConnection连接时指定Proxy, 创建Socket连接时也可以指定Proxy;而ProxySelector代表一个代理选择器,提供了对代理服务器更加灵活的控制,它可以对HTTP、HTTPS、FTP、SOCKS等进行分别设置,还可以设置不需要通过代理服务器的主机和地址。
创建了Proxy对象后,程序可以在使用URLConnection打开连接或创建Socket连接时传入一个Proxy对象,作为本次连接所用的代理服务器。
如果希望每次打开连接时总是具有默认的代理服务器,则可以借助于ProxySelector来实现。
ProxySelector代表一个代理选择器,本身是一个抽象类,可以继承并实现它,也可以使用Java提供的实现类sun.net.spi.DefaultProxySelector(这是一个未公开API,尽量避免直接使用它)。系统已经将它注册成默认的代理选择器。如果程序直接调用ProxySelector.getDefault(),则会返回DefaultProxySelector实例。