Java对于网络通讯有着非常强大的支持。不仅可以获取网络资源,传递参数到远程服务器,还可以通过Socket对象实现TCP协议,通过DatagramSocket对象实现UDP协议。同时,对于多点广播以及代理服务器也有着非常强大的支持。以下是本人在学习过程中的总结和归纳。
1. Java的基本网络支持
1.1 InetAddress
Java中的InetAddress是一个代表IP地址的对象。IP地址可以由字节数组和字符串来分别表示,InetAddress将IP地址以对象的形式进行封装,可以更方便的操作和获取其属性。InetAddress没有构造方法,可以通过两个静态方法获得它的对象。代码如下:
// 根据主机名来获取对应的InetAddress实例 InetAddress ip = InetAddress.getByName("www.oneedu.cn"); // 判断是否可达 System.out.println("oneedu是否可达:" + ip.isReachable(2000)); // 获取该InetAddress实例的IP字符串 System.out.println(ip.getHostAddress()); // 根据原始IP地址(字节数组形式)来获取对应的InetAddress实例 InetAddress local = InetAddress .getByAddress(new byte[] { 127, 0, 0, 1 }); System.out.println("本机是否可达:" + local.isReachable(5000)); // 获取该InetAddress实例对应的全限定域名 System.out.println(local.getCanonicalHostName());
1.2 URLDecoder和URLEncoder
这两个类可以别用于将application/x-www-form-urlencoded MIME类型的字符串转换为普通字符串,将普通字符串转换为这类特殊型的字符串。使用URLDecoder类的静态方法decode()用于解码,URLEncoder类的静态方法encode()用于编码。具体使用方法如下。
// 将application/x-www-form-urlencoded字符串 // 转换成普通字符串 String keyWord = URLDecoder.decode("%E6%9D%8E%E5%88%9A+j2ee", "UTF-8"); System.out.println(keyWord); // 将普通字符串转换成 // application/x-www-form-urlencoded字符串 String urlStr = URLEncoder.encode("ROR敏捷开发最佳指南", "GBK"); System.out.println(urlStr);1.3 URL和URLConnection
URL可以被认为是指向互联网资源的“指针”,通过URL可以获得互联网资源相关信息,包括获得URL的InputStream对象获取资源的信息,以及一个到URL所引用远程对象的连接URLConnection。
URLConnection对象可以向所代表的URL发送请求和读取URL的资源。通常,创建一个和URL的连接,需要如下几个步骤:
a. 创建URL对象,并通过调用openConnection方法获得URLConnection对象;
b. 设置URLConnection参数和普通请求属性;
c. 向远程资源发送请求;
d. 远程资源变为可用,程序可以访问远程资源的头字段和通过输入流来读取远程资源返回的信息。
这里需要重点讨论一下第三步:如果只是发送GET方式请求,使用connect方法建立和远程资源的连接即可;如果是需要发送POST方式的请求,则需要获取URLConnection对象所对应的输出流来发送请求。这里需要注意的是,由于GET方法的参数传递方式是将参数显式追加在地址后面,那么在构造URL对象时的参数就应当是包含了参数的完整URL地址,而在获得了URLConnection对象之后,就直接调用connect方法即可发送请求。
而POST方法传递参数时仅仅需要页面URL,而参数通过需要通过输出流来传递。另外还需要设置头字段。以下是两种方式的代码。
// 1. 向指定URL发送GET方法的请求 String urlName = url + "?" + param; 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(); // 2. 向指定URL发送POST方法的请求 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); // 获取URLConnection对象对应的输出流 out = new PrintWriter(conn.getOutputStream()); // 发送请求参数 out.print(param);
另外需要注意的是,如果既需要读取又需要发送,一定要先使用输出流,再使用输入流。因为远程资源不会主动向本地发送请求,必须要先请求资源。
2. 基于TCP协议的网络编程
TCP协议是一种可靠的通络协议,通信两端的Socket使得它们之间形成网络虚拟链路,两端的程序可以通过虚拟链路进行通讯。Java使用socket对象代表两端的通信端口,并通过socket产生的IO流来进行网络通信。
2.1 ServerSocket
在两个通信端没有建立虚拟链路之前,必须有一个通信实体首先主动监听来自另一端的请求。ServerSocket对象使用accept()方法用于监听来自客户端的Socket连接,如果收到一个客户端Socket的连接请求,该方法将返回一个与客户端Socket对应的Socket对象。如果没有连接,它将一直处于等待状态。通常情况下,服务器不应只接受一个客户端请求,而应该通过循环调用accept()不断接受来自客户端的所有请求。
这里需要注意的是,对于多次接收客户端数据的情况来说,一方面可以每次都在客户端建立一个新的Socket对象然后通过输入输出通讯,这样对于服务器端来说,每次循环所接收的内容也不一样,被认为是不同的客户端。另外,也可以只建立一次,然后在这个虚拟链路上通信,这样在服务器端一次循环的内容就是通信的全过程。
服务器端的示例代码:
// 创建一个ServerSocket,用于监听客户端Socket的连接请求 ServerSocket ss = new ServerSocket(30000); // 采用循环不断接受来自客户端的请求 while (true) { // 每当接受到客户端Socket的请求,服务器端也对应产生一个Socket Socket s = ss.accept(); // 将Socket对应的输出流包装成PrintStream PrintStream ps = new PrintStream(s.getOutputStream()); // 进行普通IO操作 ps.println("您好,您收到了服务器的新年祝福!"); // 关闭输出流,关闭Socket ps.close(); s.close(); }
2.2 Socket
使用Socket可以主动连接到服务器端,使用服务器的IP地址和端口号初始化之后,服务器端的accept便可以解除阻塞继续向下执行,这样就建立了一对互相连接的Socket。
客户端示例代码:
Socket socket = new Socket("127.0.0.1", 30000); // 将Socket对应的输入流包装成BufferedReader BufferedReader br = new BufferedReader(new InputStreamReader( socket.getInputStream())); // 进行普通IO操作 String line = br.readLine(); System.out.println("来自服务器的数据:" + line); // 关闭输入流、socket br.close(); socket.close();
2.3 使用多线程
在复杂的通讯中,使用多线程非常必要。对于服务器来说,它需要接收来自多个客户端的连接请求,处理多个客户端通讯需要并发执行,那么就需要对每一个传过来的Socket在不同的线程中进行处理,每条线程需要负责与一个客户端进行通信。以防止其中一个客户端的处理阻塞会影响到其他的线程。对于客户端来说,一方面要读取来自服务器端的数据,另一方面又要向服务器端输出数据,它们同样也需要在不同的线程中分别处理。具体代码如下,服务器端:
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(); } } }客户端:
public class MyClient { public static void main(String[] args) throws IOException { Socket s = s = new Socket("127.0.0.1", 30000); // 客户端启动ClientThread线程不断读取来自服务器的数据 new Thread(new ClientThread(s)).start(); // 获取该Socket对应的输出流 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); } } }
2.4 使用协议字符
协议字符用于标识一些字段的特定功能,用于说明传输内容的特性。它可以由用户自定义。一般情况下,可以定义一个存放这些协议字符的接口。如下:
public interface YeekuProtocol { // 定义协议字符串的长度 int PROTOCOL_LEN = 2; // 下面是一些协议字符串,服务器和客户端交换的信息 // 都应该在前、后添加这种特殊字符串。 String MSG_ROUND = "§γ"; String USER_ROUND = "∏∑"; String LOGIN_SUCCESS = "1"; String NAME_REP = "-1"; String PRIVATE_ROUND = "【"; String SPLIT_SIGN = "※"; }
在字段时可以加上这些字符,如下代码:
while (true) { String userName = JOptionPane.showInputDialog(tip + "输入用户名"); // 将用户输入的用户名的前后增加协议字符串后发送 ps.println(YeekuProtocol.USER_ROUND + userName+ YeekuProtocol.USER_ROUND); // 读取服务器的响应 String result = brServer.readLine(); // 如果用户重复,开始下次循环 if (result.equals(YeekuProtocol.NAME_REP)) { tip = "用户名重复!请重新"; continue; } // 如果服务器返回登陆成功,结束循环 if (result.equals(YeekuProtocol.LOGIN_SUCCESS)) { break; } }
收到发送来的字段时候,也再次拆分成所需要的部分,如下代码:
if (line.startsWith(YeekuProtocol.PRIVATE_ROUND) && line.endsWith(YeekuProtocol.PRIVATE_ROUND)) { // 得到真实消息 String userAndMsg = getRealMsg(line); // 以SPLIT_SIGN来分割字符串,前面部分是私聊用户,后面部分是聊天信息 String user = userAndMsg.split(YeekuProtocol.SPLIT_SIGN)[0]; String msg = userAndMsg.split(YeekuProtocol.SPLIT_SIGN)[1]; // 获取私聊用户对应的输出流,并发送私聊信息 Server.clients.get(user).println( Server.clients.getKeyByValue(ps) + "悄悄地对你说:" + msg); }
3. UDP协议的网络编程 UDP协议是一种不可靠的网络协议,它在通讯实例的两端个建立一个Socket,但这两个Socket之间并没有虚拟链路,这两个Socket只是发送和接受数据报的对象,Java提供了DatagramSocket对象作为基于UDP协议的Socket,使用DatagramPacket代表DatagramSocket发送和接收的数据报。
3.1 使用DatagramSocket发送、接收数据
DatagramSocket本身并不负责维护状态和产生IO流。它仅仅负责接收和发送数据报。使用receive(DatagramPacket p)方法接收,使用send(DatagramPacket p)方法发送。
这里需要首先明确的是,DatagramPacket对象的构造。DatagramPacket的内部实际上采用了一个字节型数组来保存数据,它的初始化方法如下:
//接收端的DatagaramSocket内部包含一个空的数组,接收传递过来的数据报中的数组信息。可以通过DatagaramSocket对象的getData()方法返回的数组来获取其中的包含的数组。 Private DatagaramSocket udpSocket=new DatagaramSocket(buf,buf.length); //发送端的DatagaramSocket内部包含一个将要传递的数组,同时需要包含目标IP和端口。如果初始化时传递的数组参数是空,可以通过调用DatagaramSocket对象的setData()方法设置内容。 Private DatagaramSocket udpSocket=new DatagaramSocket(buf,buf.length,IP,PORT); udpSocket。setData(outBuf);作为这两个方法的参数,作用和构造不同的。作为接收方法中的参数,DatagramPacket中的数组一个空的数组,用来存放接收到的DatagramPacket对象中的数组;而作为发送方法参数,DatagramPacket本身含有了目的端的IP和端口,以及存储了要发送内容的指定了长度的字节型数组。
另外,DatagramPacket对象还提供了setData(Byte[] b)和Byte[] b= getData()方法,用于设置DatagramPacket中包含的数组内容和获得其中包含数组的内容。
使用TCP和UDP通讯的编码区别:
a. 在TCP中,目标IP和端口由Socket指定包含;UDP中,目标IP由DatagramPacket包含指定,DatagramSocket只负责发送和接受。
b. 在TCP中,通讯是通过Socket获得的IO流来实现;在UDP中,则通过DatagramSocket的send和receive方法。
3.2 使用MulticastSocket实现多点广播
MulticastSocket是DatagramSocket的子类,可以将数据报以广播形式发送到数量不等的多个客户端。实现策略就是定义一个广播地址,使得每个MulticastSocket都加入到这个地址中。从而每次使用MulticastSocket发送数据报(包含的广播地址)时,所有加入了这个广播地址的MulticastSocket对象都可以收到信息。
MulticastSocket的初始化需要传递端口号作为参数,特别对于需要接受信息的端来说,它的端口号需要与发送端数据报中包含的端口号一致。具体代码如下:
// 创建用于发送、接收数据的MulticastSocket对象 // 因为该MulticastSocket对象需要接收,所以有指定端口 socket = new MulticastSocket(BROADCAST_PORT); broadcastAddress = InetAddress.getByName(BROADCAST_IP); // 将该socket加入指定的多点广播地址 socket.joinGroup(broadcastAddress); // 设置本MulticastSocket发送的数据报被回送到自身 socket.setLoopbackMode(false); // 初始化发送用的DatagramSocket,它包含一个长度为0的字节数组 outPacket = new DatagramPacket(new byte[0], 0, broadcastAddress, BROADCAST_PORT);
4. 使用代理服务器
Java中可以使用Proxy直接创建连接代理服务器,具体使用方法如下:
public class ProxyTest { Proxy proxy; URL url; URLConnection conn; // 从网络通过代理读数据 Scanner scan; PrintStream ps; // 下面是代理服务器的地址和端口, // 换成实际有效的代理服务器的地址和端口 String proxyAddress = "202.128.23.32"; int proxyPort; // 下面是你试图打开的网站地址 String urlStr = "http://www.oneedu.cn"; public void init() { try { url = new URL(urlStr); // 创建一个代理服务器对象 proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress( proxyAddress, proxyPort)); // 使用指定的代理服务器打开连接 conn = url.openConnection(proxy); // 设置超时时长。 conn.setConnectTimeout(5000); scan = new Scanner(conn.getInputStream()); // 初始化输出流 ps = new PrintStream("Index.htm"); while (scan.hasNextLine()) { String line = scan.nextLine(); // 在控制台输出网页资源内容 System.out.println(line); // 将网页资源内容输出到指定输出流 ps.println(line); } } catch (MalformedURLException ex) { System.out.println(urlStr + "不是有效的网站地址!"); } catch (IOException ex) { ex.printStackTrace(); } // 关闭资源 finally { if (ps != null) { ps.close(); } } } }
5. 编码中的问题总结
a. 双方初始化套接字以后,就等于建立了链接,表示双方互相可以知晓对方的状态。服务器端可以调用接收到的客户端套接字进行输入输出流操作,客户端可以调用自身内部的套接字对象进行输入输出操作。这样可以保持输入输出的流畅性。例如,客户端向服务器端发送消息时,可以隔一段的时间输入一段信息,然后服务器端使用循环不断的读取传过来的输入流。
b. 对于可能出现阻塞的方法,例如客户端进行循环不断读取来自服务器端的响应信息时,如果此时服务器端并没有向客户端进行输出,那么读取的方法将处于阻塞状态,直到收到信息为止才向下执行代码。那么对于这样容易产生阻塞的代码,就需要将它放在一个单独的线程中处理。
c. 有一些流是顺承的。例如,服务器端在收到客户端的消息以后,就将消息再通过输出流向其他所有服务器发送。那么,这个来自客户端的输入流和发向客户端的输出流就是顺接的关系,不必对它们分在两个不同的线程。
d. println()方法对应readLine()。
e. 在JFrame类中,一般不要将自己的代码写进main方法中,可以将代码写到自定义的方法中,然后在main方法中调用。
注: 博文来自对《疯狂java讲义》的学习。