Java对于网络通讯有着非常强大的支持。不仅可以获取网络资源,传递参数到远程服务器,还可以通过Socket对象实现TCP协议,通过DatagramSocket对象实现UDP协议。同时,对于多点广播以及代理服务器也有着非常强大的支持。以下是本人在学习过程中的总结和归纳。
1. Java的基本网络支持
1.1 InetAddress Java中的InetAddress是一个代表IP地址的对象。IP地址可以由字节数组和字符串来分别表示,InetAddress将IP地址以对象的形式进行封装,可以更方便的操作和获取其属性。InetAddress没有构造方法,可以通过两个静态方法获得它的对象。代码如下:
InetAddress ip = InetAddress.getByName( "www.oneedu.cn" ); System.out.println( "oneedu是否可达:" + ip.isReachable( 2000 )); System.out.println(ip.getHostAddress()); InetAddress local = InetAddress.getByAddress( new byte [] { 127 , 0 , 0 , 1 }); System.out.println( "本机是否可达:" + local.isReachable( 5000 )); System.out.println(local.getCanonicalHostName());
1.2 URLDecoder和URLEncoder
这两个类可以别用于将application/x-www-form-urlencoded MIME类型的字符串转换为普通字符串,将普通字符串转换为这类特殊型的字符串。使用URLDecoder类的静态方法decode()用于解码,URLEncoder类的静态方法encode()用于编码。具体使用方法如下。
String keyWord = URLDecoder.decode( "%E6%9D%8E%E5%88%9A+j2ee" , "UTF-8" ); System.out.println(keyWord); 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,而参数通过需要通过输出流来传递。另外还需要设置头字段。以下是两种方式的代码。
String urlName = url + "?" + param; URL realUrl = new URL(urlName); 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(); URL realUrl = new 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)" ); conn.setDoOutput( true ); conn.setDoInput( true ); 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 ss = new ServerSocket( 30000 ); while ( true ) { Socket s = ss.accept(); PrintStream ps = new PrintStream(s.getOutputStream()); ps.println( "您好,您收到了服务器的新年祝福!" ); ps.close(); s.close(); }
2.2 Socket 使用Socket可以主动连接到服务器端,使用服务器的IP地址和端口号初始化之后,服务器端的accept便可以解除阻塞继续向下执行,这样就建立了一对互相连接的Socket。 客户端示例代码:
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();
2.3 使用多线程
在复杂的通讯中,使用多线程非常必要。对于服务器来说,它需要接收来自多个客户端的连接请求,处理多个客户端通讯需要并发执行,那么就需要对每一个传过来的Socket在不同的线程中进行处理,每条线程需要负责与一个客户端进行通信。以防止其中一个客户端的处理阻塞会影响到其他的线程。对于客户端来说,一方面要读取来自服务器端的数据,另一方面又要向服务器端输出数据,它们同样也需要在不同的线程中分别处理。
具体代码如下,服务器端:
public class MyServer { 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); 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 ); 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 ) { 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); 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的内部实际上采用了一个字节型数组来保存数据,它的初始化方法如下:
Private DatagaramSocket udpSocket= new DatagaramSocket(buf,buf.length); 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的初始化需要传递端口号作为参数,特别对于需要接受信息的端来说,它的端口号需要与发送端数据报中包含的端口号一致。具体代码如下:
socket = new MulticastSocket(BROADCAST_PORT); broadcastAddress = InetAddress.getByName(BROADCAST_IP); socket.joinGroup(broadcastAddress); socket.setLoopbackMode( false ); 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方法中调用。