Java網(wǎng)絡(luò)編程
Java對(duì)于網(wǎng)絡(luò)通訊有著非常強(qiáng)大的支持。不僅可以獲取網(wǎng)絡(luò)資源,傳遞參數(shù)到遠(yuǎn)程服務(wù)器,還可以通過(guò)Socket對(duì)象實(shí)現(xiàn)TCP協(xié)議,通過(guò)DatagramSocket對(duì)象實(shí)現(xiàn)UDP協(xié)議。同時(shí),對(duì)于多點(diǎn)廣播以及代理服務(wù)器也有著非常強(qiáng)大的支持。以下是本人在學(xué)習(xí)過(guò)程中的總結(jié)和歸納。
1. Java的基本網(wǎng)絡(luò)支持
1.1 InetAddress
Java中的InetAddress是一個(gè)代表IP地址的對(duì)象。IP地址可以由字節(jié)數(shù)組和字符串來(lái)分別表示,InetAddress將IP地址以對(duì)象的形式進(jìn)行封裝,可以更方便的操作和獲取其屬性。InetAddress沒(méi)有構(gòu)造方法,可以通過(guò)兩個(gè)靜態(tài)方法獲得它的對(duì)象。代碼如下:
-
- InetAddress ip = InetAddress.getByName("www.oneedu.cn");
-
- System.out.println("oneedu是否可達(dá):" + ip.isReachable(2000));
-
- System.out.println(ip.getHostAddress());
-
- InetAddress local = InetAddress.getByAddress(new byte[]
- {127,0,0,1});
- System.out.println("本機(jī)是否可達(dá):" + local.isReachable(5000));
-
- System.out.println(local.getCanonicalHostName());
|
1.2 URLDecoder和URLEncoder 這兩個(gè)類(lèi)可以別用于將application/x-www-form-urlencoded MIME類(lèi)型的字符串轉(zhuǎn)換為普通字符串,將普通字符串轉(zhuǎn)換為這類(lèi)特殊型的字符串。使用URLDecoder類(lèi)的靜態(tài)方法decode()用于解碼,URLEncoder類(lèi)的靜態(tài)方法encode()用于編碼。具體使用方法如下。
-
-
- String keyWord = URLDecoder.decode(
- "%E6%9D%8E%E5%88%9A+j2ee", "UTF-8");
- System.out.println(keyWord);
-
-
- String urlStr = URLEncoder.encode(
- "ROR敏捷開(kāi)發(fā)最佳指南" , "GBK");
- System.out.println(urlStr);
|
1.3 URL和URLConnection URL可以被認(rèn)為是指向互聯(lián)網(wǎng)資源的“指針”,通過(guò)URL可以獲得互聯(lián)網(wǎng)資源相關(guān)信息,包括獲得URL的InputStream對(duì)象獲取資源的信息,以及一個(gè)到URL所引用遠(yuǎn)程對(duì)象的連接URLConnection。 URLConnection對(duì)象可以向所代表的URL發(fā)送請(qǐng)求和讀取URL的資源。通常,創(chuàng)建一個(gè)和URL的連接,需要如下幾個(gè)步驟: a. 創(chuàng)建URL對(duì)象,并通過(guò)調(diào)用openConnection方法獲得URLConnection對(duì)象; b. 設(shè)置URLConnection參數(shù)和普通請(qǐng)求屬性; c. 向遠(yuǎn)程資源發(fā)送請(qǐng)求; d. 遠(yuǎn)程資源變?yōu)榭捎茫绦蚩梢栽L問(wèn)遠(yuǎn)程資源的頭字段和通過(guò)輸入流來(lái)讀取遠(yuǎn)程資源返回的信息。 這里需要重點(diǎn)討論一下第三步:如果只是發(fā)送GET方式請(qǐng)求,使用connect方法建立和遠(yuǎn)程資源的連接即可;如果是需要發(fā)送POST方式的請(qǐng)求,則需要獲取URLConnection對(duì)象所對(duì)應(yīng)的輸出流來(lái)發(fā)送請(qǐng)求。這里需要注意的是,由于GET方法的參數(shù)傳遞方式是將參數(shù)顯式追加在地址后面,那么在構(gòu)造URL對(duì)象時(shí)的參數(shù)就應(yīng)當(dāng)是包含了參數(shù)的完整URL地址,而在獲得了URLConnection對(duì)象之后,就直接調(diào)用connect方法即可發(fā)送請(qǐng)求。而POST方法傳遞參數(shù)時(shí)僅僅需要頁(yè)面URL,而參數(shù)通過(guò)需要通過(guò)輸出流來(lái)傳遞。另外還需要設(shè)置頭字段。以下是兩種方式的代碼。
-
- 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);
-
|
另外需要注意的是,如果既需要讀取又需要發(fā)送,一定要先使用輸出流,再使用輸入流。因?yàn)檫h(yuǎn)程資源不會(huì)主動(dòng)向本地發(fā)送請(qǐng)求,必須要先請(qǐng)求資源。2. 基于TCP協(xié)議的網(wǎng)絡(luò)編程 TCP協(xié)議是一種可靠的通絡(luò)協(xié)議,通信兩端的Socket使得它們之間形成網(wǎng)絡(luò)虛擬鏈路,兩端的程序可以通過(guò)虛擬鏈路進(jìn)行通訊。Java使用socket對(duì)象代表兩端的通信端口,并通過(guò)socket產(chǎn)生的IO流來(lái)進(jìn)行網(wǎng)絡(luò)通信。
2.1 ServerSocket
在兩個(gè)通信端沒(méi)有建立虛擬鏈路之前,必須有一個(gè)通信實(shí)體首先主動(dòng)監(jiān)聽(tīng)來(lái)自另一端的請(qǐng)求。ServerSocket對(duì)象使用accept()方法用于監(jiān)聽(tīng)來(lái)自客戶(hù)端的Socket連接,如果收到一個(gè)客戶(hù)端Socket的連接請(qǐng)求,該方法將返回一個(gè)與客戶(hù)端Socket對(duì)應(yīng)的Socket對(duì)象。如果沒(méi)有連接,它將一直處于等待狀態(tài)。通常情況下,服務(wù)器不應(yīng)只接受一個(gè)客戶(hù)端請(qǐng)求,而應(yīng)該通過(guò)循環(huán)調(diào)用accept()不斷接受來(lái)自客戶(hù)端的所有請(qǐng)求。
這里需要注意的是,對(duì)于多次接收客戶(hù)端數(shù)據(jù)的情況來(lái)說(shuō),一方面可以每次都在客戶(hù)端建立一個(gè)新的Socket對(duì)象然后通過(guò)輸入輸出通訊,這樣對(duì)于服務(wù)器端來(lái)說(shuō),每次循環(huán)所接收的內(nèi)容也不一樣,被認(rèn)為是不同的客戶(hù)端。另外,也可以只建立一次,然后在這個(gè)虛擬鏈路上通信,這樣在服務(wù)器端一次循環(huán)的內(nèi)容就是通信的全過(guò)程。
服務(wù)器端的示例代碼:
-
- ServerSocket ss = new ServerSocket(30000);
-
- while (true)
- {
-
- Socket s = ss.accept();
-
- PrintStream ps = new PrintStream(s.getOutputStream());
-
- ps.println("您好,您收到了服務(wù)器的新年祝福!");
-
- ps.close();
- s.close();
- }
|
2.2 Socket
使用Socket可以主動(dòng)連接到服務(wù)器端,使用服務(wù)器的IP地址和端口號(hào)初始化之后,服務(wù)器端的accept便可以解除阻塞繼續(xù)向下執(zhí)行,這樣就建立了一對(duì)互相連接的Socket。
客戶(hù)端示例代碼:
- Socket socket = new Socket("127.0.0.1" , 30000);
-
- BufferedReader br = new BufferedReader(
- new InputStreamReader(socket.getInputStream()));
-
- String line = br.readLine();
- System.out.println("來(lái)自服務(wù)器的數(shù)據(jù):" + line);
-
- br.close();
- socket.close();
|
2.3 使用多線程 在復(fù)雜的通訊中,使用多線程非常必要。對(duì)于服務(wù)器來(lái)說(shuō),它需要接收來(lái)自多個(gè)客戶(hù)端的連接請(qǐng)求,處理多個(gè)客戶(hù)端通訊需要并發(fā)執(zhí)行,那么就需要對(duì)每一個(gè)傳過(guò)來(lái)的Socket在不同的線程中進(jìn)行處理,每條線程需要負(fù)責(zé)與一個(gè)客戶(hù)端進(jìn)行通信。以防止其中一個(gè)客戶(hù)端的處理阻塞會(huì)影響到其他的線程。對(duì)于客戶(hù)端來(lái)說(shuō),一方面要讀取來(lái)自服務(wù)器端的數(shù)據(jù),另一方面又要向服務(wù)器端輸出數(shù)據(jù),它們同樣也需要在不同的線程中分別處理。具體代碼如下,服務(wù)器端:
- 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();
- }
- }
- }
|
客戶(hù)端:
- 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 使用協(xié)議字符
協(xié)議字符用于標(biāo)識(shí)一些字段的特定功能,用于說(shuō)明傳輸內(nèi)容的特性。它可以由用戶(hù)自定義。一般情況下,可以定義一個(gè)存放這些協(xié)議字符的接口。如下:
- 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 = "※";
- }
|
在字段時(shí)可以加上這些字符,如下代碼:
- while(true)
- {
- String userName = JOptionPane.showInputDialog(tip + "輸入用戶(hù)名");
-
- ps.println(YeekuProtocol.USER_ROUND + userName
- + YeekuProtocol.USER_ROUND);
-
- String result = brServer.readLine();
-
- if (result.equals(YeekuProtocol.NAME_REP))
- {
- tip = "用戶(hù)名重復(fù)!請(qǐng)重新";
- continue;
- }
-
- if (result.equals(YeekuProtocol.LOGIN_SUCCESS))
- {
- break;
- }
- }
|
收到發(fā)送來(lái)的字段時(shí)候,也再次拆分成所需要的部分,如下代碼:
- 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) + "悄悄地對(duì)你說(shuō):" + msg);
- }
|
3. UDP協(xié)議的網(wǎng)絡(luò)編程 UDP協(xié)議是一種不可靠的網(wǎng)絡(luò)協(xié)議,它在通訊實(shí)例的兩端個(gè)建立一個(gè)Socket,但這兩個(gè)Socket之間并沒(méi)有虛擬鏈路,這兩個(gè)Socket只是發(fā)送和接受數(shù)據(jù)報(bào)的對(duì)象,Java提供了DatagramSocket對(duì)象作為基于UDP協(xié)議的Socket,使用DatagramPacket代表DatagramSocket發(fā)送和接收的數(shù)據(jù)報(bào)。
3.1 使用DatagramSocket發(fā)送、接收數(shù)據(jù)
DatagramSocket本身并不負(fù)責(zé)維護(hù)狀態(tài)和產(chǎn)生IO流。它僅僅負(fù)責(zé)接收和發(fā)送數(shù)據(jù)報(bào)。使用receive(DatagramPacket p)方法接收,使用send(DatagramPacket p)方法發(fā)送。
這里需要首先明確的是,DatagramPacket對(duì)象的構(gòu)造。DatagramPacket的內(nèi)部實(shí)際上采用了一個(gè)字節(jié)型數(shù)組來(lái)保存數(shù)據(jù),它的初始化方法如下:
-
- Private DatagaramSocket udpSocket=new DatagaramSocket(buf,buf.length);
-
- Private DatagaramSocket udpSocket=new DatagaramSocket(buf,buf.length,IP,PORT);
- udpSocket。setData(outBuf);
|
作為這兩個(gè)方法的參數(shù),作用和構(gòu)造不同的。作為接收方法中的參數(shù),DatagramPacket中的數(shù)組一個(gè)空的數(shù)組,用來(lái)存放接收到的DatagramPacket對(duì)象中的數(shù)組;而作為發(fā)送方法參數(shù),DatagramPacket本身含有了目的端的IP和端口,以及存儲(chǔ)了要發(fā)送內(nèi)容的指定了長(zhǎng)度的字節(jié)型數(shù)組。 另外,DatagramPacket對(duì)象還提供了setData(Byte[] b)和Byte[] b= getData()方法,用于設(shè)置DatagramPacket中包含的數(shù)組內(nèi)容和獲得其中包含數(shù)組的內(nèi)容。 使用TCP和UDP通訊的編碼區(qū)別: a. 在TCP中,目標(biāo)IP和端口由Socket指定包含;UDP中,目標(biāo)IP由DatagramPacket包含指定,DatagramSocket只負(fù)責(zé)發(fā)送和接受。 b. 在TCP中,通訊是通過(guò)Socket獲得的IO流來(lái)實(shí)現(xiàn);在UDP中,則通過(guò)DatagramSocket的send和receive方法。
3.2 使用MulticastSocket實(shí)現(xiàn)多點(diǎn)廣播
MulticastSocket是DatagramSocket的子類(lèi),可以將數(shù)據(jù)報(bào)以廣播形式發(fā)送到數(shù)量不等的多個(gè)客戶(hù)端。實(shí)現(xiàn)策略就是定義一個(gè)廣播地址,使得每個(gè)MulticastSocket都加入到這個(gè)地址中。從而每次使用MulticastSocket發(fā)送數(shù)據(jù)報(bào)(包含的廣播地址)時(shí),所有加入了這個(gè)廣播地址的MulticastSocket對(duì)象都可以收到信息。
MulticastSocket的初始化需要傳遞端口號(hào)作為參數(shù),特別對(duì)于需要接受信息的端來(lái)說(shuō),它的端口號(hào)需要與發(fā)送端數(shù)據(jù)報(bào)中包含的端口號(hào)一致。具體代碼如下:
-
-
- 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. 使用代理服務(wù)器 Java中可以使用Proxy直接創(chuàng)建連接代理服務(wù)器,具體使用方法如下:
- 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 + "不是有效的網(wǎng)站地址!");
- }
- catch(IOException ex)
- {
- ex.printStackTrace();
- }
-
- finally
- {
- if (ps != null)
- {
- ps.close();
- }
- }
- }
-
- }
|
5. 編碼中的問(wèn)題總結(jié)
a. 雙方初始化套接字以后,就等于建立了鏈接,表示雙方互相可以知曉對(duì)方的狀態(tài)。服務(wù)器端可以調(diào)用接收到的客戶(hù)端套接字進(jìn)行輸入輸出流操作,客戶(hù)端可以調(diào)用自身內(nèi)部的套接字對(duì)象進(jìn)行輸入輸出操作。這樣可以保持輸入輸出的流暢性。例如,客戶(hù)端向服務(wù)器端發(fā)送消息時(shí),可以隔一段的時(shí)間輸入一段信息,然后服務(wù)器端使用循環(huán)不斷的讀取傳過(guò)來(lái)的輸入流。
b. 對(duì)于可能出現(xiàn)阻塞的方法,例如客戶(hù)端進(jìn)行循環(huán)不斷讀取來(lái)自服務(wù)器端的響應(yīng)信息時(shí),如果此時(shí)服務(wù)器端并沒(méi)有向客戶(hù)端進(jìn)行輸出,那么讀取的方法將處于阻塞狀態(tài),直到收到信息為止才向下執(zhí)行代碼。那么對(duì)于這樣容易產(chǎn)生阻塞的代碼,就需要將它放在一個(gè)單獨(dú)的線程中處理。
c. 有一些流是順承的。例如,服務(wù)器端在收到客戶(hù)端的消息以后,就將消息再通過(guò)輸出流向其他所有服務(wù)器發(fā)送。那么,這個(gè)來(lái)自客戶(hù)端的輸入流和發(fā)向客戶(hù)端的輸出流就是順接的關(guān)系,不必對(duì)它們分在兩個(gè)不同的線程。
d. println()方法對(duì)應(yīng)readLine()。
e. 在JFrame類(lèi)中,一般不要將自己的代碼寫(xiě)進(jìn)main方法中,可以將代碼寫(xiě)到自定義的方法中,然后在main方法中調(diào)用。