一、網絡編程概述
(1)、網絡通訊三要素
1、IP地址:2、端口號:網絡中設備的標識
IP有4位和6位。6位包含字母。
不宜記憶,可用主機名
本地默認IP:127.0.0.1主機名:localhost
3、傳輸協議:數據要發送到對方指定的應用程序上,爲了識別這些應用程序,所以給這些網絡應用程序進行了標識,爲了方便稱呼這個數字,就叫做端口。(邏輯端口)
用於標識進程的邏輯地址,不同進程的標識。
有效端口:0-65535,其中,0-1024系統使用或者保留端口子
定義通訊規則,這個通訊規則就稱爲協議。國際組織定義了通用協議TCP/IP。
常見協議:TCP,UDP
(2)、網絡模型
OSI參考模型
TCP/IP參考模型
(3)、IP的使用
package cn.hwk; import java.net.*; public class IPDemo { public static void main(String[] args) throws Exception { try { // 本地主機ip地址對象 // InetAddress i = InetAddress.getLocalHost(); // 返回 IP 地址字符串(以文本表現形式)。 // System.out.println("Address:" + i.getHostAddress()); // 獲取此 IP 地址的主機名。 // System.out.println("Name:" + i.getHostName()); // 獲取此 IP 地址的主機名。 // System.out.println(i.hashCode()); // 獲取其他主機ip地址對象 InetAddress ia = InetAddress.getByName("www.taobao.com"); System.out.println("Address:" + ia.getHostAddress()); System.out.println("Name:" + ia.getHostName()); } catch (UnknownHostException e) { e.printStackTrace(); } } }
二、DDP、TCP、Socket
(1)、UDP:(2)、TCP:將數據及源和目的封裝成數據包中,不需要建立連接
每個數據包的大小限制在64k內
因爲無連接,是不可靠協議
不需要建立連接,速度快
理解:就像到郵局中寄包裹一樣,包要寄的東西打包,說明壓迫寄往的地址和收件人(端口)
建立連接,形成傳輸數據的通道
在連接中進行大數據傳輸
通過三次握手完成連接,是可靠協議
必須建立連接,效率會稍低
理解:打電話,需要先撥通對方號碼(向對方發出通話請求),對方接聽(表示對方收到請求),然後說話(告訴你他已收到請求)
(3)、Socket:
就是爲網絡服務提供一種機制
通信的兩端都有Socket。//只要有了它,才能進行連接,連接後纔有通路,現有碼頭,再有船
網絡通信其實就是Socket間的通信
數據在兩個Socket間通過IO傳輸
理解:有了碼頭,船才能在碼頭間來往。
三、UDP傳輸
(1)、關於UDP的傳輸,Java智能光提供了兩個類:DatagramSocket和DatagramPacket建立發送端、接收端。
調用Socket的發送接收方法。
關閉Socket。
(2)、DatagramSocket:此類表示用來發送和接收數據報包的套接字。
數據報套接字是包投遞服務的發送或接收點。每個在數據報套接字上發送或接收的包都是單獨編址和路由的。從一臺機器發送到另一臺機器的多個包可能選擇不同的路
由,也可能按不同的順序到達。
在 DatagramSocket 上總是啓用 UDP 廣播發送。爲了接收廣播包,應該將 DatagramSocket 綁定到通配符地址。在某些實現中,將 DatagramSocket 綁定到一個更加
具體的地址時廣播包也可以被接收。
示例:DatagramSocket s = new DatagramSocket(null); s.bind(new InetSocketAddress(8888));
這等價於:DatagramSocket s = new DatagramSocket(8888);
兩個例子都能創建能夠在 UDP 8888 端口上接收廣播的 DatagramSocket。
方法摘要:
receive(DatagramPacket p) 從此套接字接收數據報包。
void send(DatagramPacket p) 從此套接字發送數據報包。
(3)、DatagramPacket:此類表示數據報包。
數據報包用來實現無連接包投遞服務。每條報文僅根據該包中包含的信息從一臺機器路由到另一臺機器。從一臺機器發送到另一臺機器的多個包可能選擇不同的路由,
也可能按不同的順序到達。不對包投遞做出保證。
構造方法摘要:
DatagramPacket(byte[] buf, int length) :構造 DatagramPacket,用來接收長度爲 length 的數據包。
DatagramPacket(byte[] buf, int length, InetAddress address, int port) :構造數據報包,用來將長度爲 length 的包發送到指定主機上的指定端口號。
DatagramPacket(byte[] buf, int offset, int length) :構造 DatagramPacket,用來接收長度爲 length 的包,在緩衝區中指定了偏移量。
DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port) :構造數據報包,用來將長度爲 length 偏移量爲 offset 的包發送到指定主機上的指定端
口號。
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) :構造數據報包,用來將長度爲 length 偏移量爲 offset 的包發送到指定主機上的指定端口
號。
DatagramPacket(byte[] buf, int length, SocketAddress address) :構造數據報包,用來將長度爲 length 的包發送到指定主機上的指定端口號。
(4)、UDP建立發送端
package cn.hwk; //定義一個發送端 import java.net.*; /*需求:通過udp傳輸方式,將一段文字數據發送出去 * 就是定義一個發送端。 * 思路: * 1.建立udpsocket服務 * 2.提供數據,並且將數據封裝到數據包中 * 3.通過scoket服務的發送功能,將數據包發送出去 * 4.關閉資源 * */ public class UdpSend { public static void main(String[] args) throws Exception { // 1.建立udpsocket服務,通過DatagramSocket對象。建立發送端點 DatagramSocket ds = new DatagramSocket(9999); // 2.提供數據,並且將數據封裝到數據包中DatagramPacket(byte[] buf, int length, InetAddress // address, int port) byte[] buf = "upd你好啊".getBytes(); DatagramPacket dp = new DatagramPacket(buf, buf.length, InetAddress.getByName("192.168.1.102"), 10000); // 3.通過scoket服務的發送功能,將數據包發送出去 ds.send(dp); // 4.關閉資源 ds.close(); } }
(5)、UDP建立接收端
package cn.hwk; //定義一個udp的接收端 import java.net.*; /* 需求:定義一個應用程序。用於接受udp協議傳輸的數據並處理 * 就是定義一個udp的接收端 * * 思路: * 1.定義一個udpsocket服務.通常會監聽一個端口。就是給這個接受網絡應用程序定義數字標識。 * 方便於明確哪些數據過來,該應用程序可以處理。 * * 2.定義一個數據包,因爲要存儲接受到的字節數據 * 因爲數據包對象中有更多的功能可以提取字節數據中的不同數據信息 * 3.通過socket服務的receive方法,將收到的數據存入已定義好的數據包中 * 4.通過數據包對象的特有功能,將這些不同的數據取出,並打印在控制檯上 * 5.關閉資源 */ public class UpdReceive { public static void main(String[] args) throws Exception { // 1.創建udpcocket,建立接受端點 DatagramSocket ds = new DatagramSocket(10000); while (true) { // 2.定義一個數據包,存儲接受到的字節數據DatagramPacket(byte[] buf, int length) byte[] buf = new byte[1024 * 2]; DatagramPacket dp = new DatagramPacket(buf, buf.length); // 3.通過socket服務的receive方法,將收到的數據存入已定義好的數據包中 ds.receive(dp);// 阻塞式方法 // 4.通過數據包對象的特有功能,將這些不哦她難過的數據取出,並打印在控制檯上 String ip = dp.getAddress().getHostAddress(); String date = new String(dp.getData(), 0, dp.getLength()); int port = dp.getPort(); System.out.println(ip + "::" + date + "::" + port); } // 5,關閉資源 // ds.close(); } }
(6)、鍵盤錄入在UDP通訊中的應用。
發送端:
package cn.hwk; import java.io.*; import java.net.*; public class UdpSend2 { public static void main(String[] args) throws Exception { DatagramSocket ds = new DatagramSocket(); BufferedReader bufr = new BufferedReader(new InputStreamReader( System.in)); String line = null; while ((line = bufr.readLine()) != null) { if ("over".equals(line)) break; byte[] buf = line.getBytes(); DatagramPacket dp = new DatagramPacket(buf, buf.length, InetAddress.getByName("192.168.1.102"), 10000); ds.send(dp); } ds.close(); } }
接收端:
package cn.hwk; import java.net.*; import java.io.*; public class UdpReceive2 { public static void main(String[] args) throws Exception { DatagramSocket ds = new DatagramSocket(10000); while (true) { byte[] buf = new byte[1024]; DatagramPacket dp = new DatagramPacket(buf, buf.length); ds.receive(dp); String ip = dp.getAddress().getHostAddress(); String date = new String(dp.getData(), 0, dp.getLength()); System.out.println(ip + ":::" + date); } } }
(7)、一個簡單的聊天程序
package cn.hwk; /* 編寫一個連天程序: * * 有接收數據的部分,和發送數據的部分 * 這兩個部分要同時執行 * 需要用到多線程技術 * 一個線程控制發送,一個線程控制接收 * * 因爲收和發的動作是不一致的,所以要定義兩個run方法。 * 而且這兩個方法要封裝到不同的類中 */ import java.io.*; import java.net.*; public class ChatDemo { public static void main(String[] args) { try { DatagramSocket sendsocket = new DatagramSocket();// 發送端 DatagramSocket recesocket = new DatagramSocket(10001);// 接受端必須制定端口號 Thread t1 = new Thread(new Send(sendsocket));// 發送端線程 Thread t2 = new Thread(new Rece(recesocket));// 接收端 線程 t1.start(); t2.start(); } catch (Exception e) { e.printStackTrace(); } } } /* * 發送端類 */ class Send implements Runnable { private DatagramSocket ds;// udpsocket服務 public Send(DatagramSocket ds) {// 初始化一個發送端DatagramSocket對象 this.ds = ds; } // 重寫run方法,鍵盤錄入io操作 public void run() { BufferedReader br = new BufferedReader(new InputStreamReader(System.in));// 鍵盤錄入 try { String line = null; while ((line = br.readLine()) != null) {// 循環讀取 if (line.equals("86"))// 自定義結束標記 break; // 構造數據包,封裝了數據,接受ip,接受端口,存儲數據 byte[] b = line.getBytes(); DatagramPacket dp = new DatagramPacket(b, b.length, InetAddress.getByName("192.168.1.100"), 10001);// 封裝數據包 ds.send(dp);// 發送 } } catch (Exception e) { try { throw new Exception("發送端失敗"); } catch (Exception e1) { e1.printStackTrace(); } } finally { ds.close();// 關閉資源 } } } /* * 接收發送端類 */ class Rece implements Runnable { private DatagramSocket ds; public Rece(DatagramSocket ds) {// 初始化一個接受端DatagramSocket對象,接受數據 必須明確一個端口號 this.ds = ds; } public void run() {// 重寫線程run方法 while (true) { byte[] b = new byte[1024]; DatagramPacket dp = new DatagramPacket(b, b.length);// 創建數據包,用於存儲接受的數據 try { ds.receive(dp);// 阻塞式方法,將接受的數據存儲到數據包中 // 通過數據包獲取ip,和轉換後的字符串 String ip = dp.getAddress().getHostName();// 通過數據包的方法解析包中的數據 String data = new String(dp.getData(), 0, dp.getLength()); System.out.println("ip" + ip + "data" + data); } catch (Exception e) { try { throw new Exception("發送端失敗"); } catch (Exception e1) { e1.printStackTrace(); } } finally { ds.close();// 關閉資源 } } } }
四、TCP傳輸
(1)、關於TCP的傳輸,Java中提供了兩個類,Socket(客戶端對象)和ServerSocket(服務端對象)
步驟:
建立客戶端和服務端
建立連接後,通過Socket中的io流進行數據的傳輸
關閉Socket
同樣,客戶端與服務器端是兩個獨立的應用程序
客戶端一建立,就要連接服務端
(2)、Socket(客戶端對象):此類實現客戶端套接字(也可以就叫“套接字”)。套接字是兩臺機器間通信的端點。
ServerSocket(服務端對象):此類實現服務器套接字。服務器套接字等待請求通過網絡傳入。它基於該請求執行某些操作,然後可能向請求者返回結果。
(3)、TCP建立客服端
客戶端:
通過查閱socket對象,發現在該對象建立時,就可以去連接指定主機
因爲tcp是面向連接的,所以在建立連接socket服務時。
就要有服務端存在,並連接成功。形成通路後,在該通道進行數據的傳輸。
package cn.hwk; import java.io.*; import java.net.*; /* * * 需求:給服務端發送一個文本數據 * * 步驟: * 1,創建TCP 客戶端Socket服務,使用Socket對象。並指定要連接的主機和端口 * 建議該對象一創建就明確目的地。要連接主機 * 2,如果連接成功,說明數據的通道已建立 * 該通道是Socket流,是底層的,既然是流,說明這裏既有輸入又有輸出 * 可以通過getOutputStream(),和getInputSteam()來獲取兩個字節流 * 3,使用輸出流。將數據寫出 * 4,關閉資源 */ public class TcpClient { public static void main(String[] args) { try { // 創建客戶端的socket服務,指定目的主機和端口 Socket s = new Socket("192.168.1.102", 10001); // 爲了發送數據,應該獲取socket流中的輸出流 OutputStream out = s.getOutputStream(); out.write("aaa".getBytes()); s.close();// socket關閉,流也關閉了 } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
(4)、TCP建立服務端
package cn.hwk; import java.io.*; import java.net.*; public class TcpServer { public static void main(String[] args) { Socket s = null; ServerSocket ss = null; try { // 建立服務端的socket,並監聽一個端口 ss = new ServerSocket(10001); // 通過accept方法獲取連接過來的客戶端對象 s = ss.accept();// 阻塞式的方法 String ip = s.getInetAddress().getHostAddress(); System.out.println("ip:" + ip); // 獲取客戶端發送過來的數據,那麼要使用客戶端對象的讀取流方法來讀取數據 InputStream in = s.getInputStream(); byte[] buf = new byte[1024]; System.out.println(new String(buf, 0, buf.length)); } catch (IOException e) { e.printStackTrace(); } finally { try { s.close(); } catch (IOException e) { e.printStackTrace(); }// 不關自己,關閉客戶端 try { ss.close(); } catch (IOException e) { e.printStackTrace(); } } } }
(5)、TCP建立交互方式
1、客戶端
package cn.hwk; import java.io.*; import java.net.*; /* * 客服端: * 1,建立socket服務指定要連接主機和端口子 * 2,獲取socket流中的輸出流,將數據寫到該流中,通過網絡發送給服務端 */ public class TcpClient2 { public static void main(String[] args) { try { Socket s = new Socket("192.168.1.102", 10002); OutputStream out = s.getOutputStream(); out.write("服務端,你好".getBytes()); InputStream in = s.getInputStream(); byte[] buf = new byte[1024]; int len = in.read(); System.out.println(new String(buf, 0, len)); s.close(); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
2、服務端
package cn.hwk; /* * 通過tcp服務的思路: * 1,建立服務端socket服務。通過ServerSocket對象 * 2,服務端必須對外提供一個藉口,否則客戶端無法連接 * 3,獲取鏈接過來的客戶端對象 * 4,通過客戶端對象獲取socket流讀取客戶端發來的數據 並打印在控制檯上 * 5,關閉資源,管客戶端,管服務端 */ import java.io.*; import java.net.*; public class TcpServer2 { public static void main(String[] args) { // 服務端接受客戶端發送過來的數據,並打印在控制檯上 try { // 1、創建服務端對象 ServerSocket ss = new ServerSocket(10002); // 獲取連接過來的客戶端對象 Socket s = ss.accept(); // 2、獲取ip String ip = s.getInetAddress().getHostAddress(); System.out.println("ip:" + ip); // 3、通過socket對象獲取輸入流,要對客戶端發過來的數據 InputStream in = s.getInputStream(); byte[] buf = new byte[1024]; int len = in.read(buf); System.out.println(new String(buf, 0, len)); // 4、使用客戶端socket對象的輸出流給客戶端返回數據 OutputStream out = s.getOutputStream(); out.write("收到".getBytes()); s.close(); ss.close(); } catch (IOException e) { e.printStackTrace(); } } }
(6)、TCP複製文件
客戶端:
package cn.hwk; import java.io.*; import java.net.*; public class TestClient { public static void main(String[] args) throws Exception { Socket s = new Socket("192.168.1.102", 10006); BufferedReader bufr = new BufferedReader(new FileReader("c:\\IPDemo.java")); PrintWriter out = new PrintWriter(s.getOutputStream(), true); String line = null; while ((line = bufr.readLine()) != null) { out.println(line); } out.println("over"); BufferedReader bufIn = new BufferedReader(new InputStreamReader( s.getInputStream())); String str = bufIn.readLine(); bufIn.close(); s.close(); } }
服務端:
package cn.hwk; import java.io.*; import java.net.*; public class TestServer { public static void main(String[] args) throws Exception { ServerSocket ss = new ServerSocket(10006); Socket s = ss.accept(); BufferedReader bufIn = new BufferedReader(new InputStreamReader( s.getInputStream())); PrintWriter out = new PrintWriter(new FileWriter("c:\\server.txt"), true); String line = null; while ((line = bufIn.readLine()) != null) { if ("over".equals(line)) break; out.println(line); } PrintWriter pw = new PrintWriter(s.getOutputStream(), true); pw.println("上傳成功"); out.close(); s.close(); ss.close(); } }
(7)、TCP單張上傳圖片
客戶端:
package cn.hwk; /* * 單人上傳圖片 * 客戶端。 * 1,服務端點 * 2,去讀客戶端已有的圖片數據 * 3,通過socket輸出流將數據發送給服務端 * 4,讀取服務端反饋信息 * 5,關閉 */ import java.net.*; import java.io.*; public class PicClient { public static void main(String[] args) throws Exception, IOException { Socket s = new Socket("192.168.1.102", 10008); FileInputStream fis = new FileInputStream("c:\\1.jpg"); OutputStream out = s.getOutputStream(); byte[] buf = new byte[1024]; int len = 0; while ((len = fis.read(buf)) != -1) { out.write(buf, 0, len); } // 告訴服務端數據已寫完 s.shutdownOutput(); InputStream in = s.getInputStream(); byte[] bufin = new byte[1024]; int num = in.read(bufin); System.out.println(new String(bufin, 0, num)); fis.close(); s.close(); } }
服務端:
package cn.hwk; import java.io.*; import java.net.*; /*服務端 */ class PicServer { public static void main(String[] args) throws Exception { ServerSocket ss = new ServerSocket(10008); Socket s = ss.accept(); InputStream in = s.getInputStream(); FileOutputStream fos = new FileOutputStream("c:\\server.jpg"); byte[] buf = new byte[1024]; int len = 0; while ((len = in.read(buf)) != -1) { fos.write(buf, 0, len); } OutputStream out = s.getOutputStream(); out.write("上傳成功".getBytes()); fos.close(); s.close(); ss.close(); } }
(8)、TCP客戶端併發上傳圖片
這個服務端有個侷限性。當a客戶端連接上以後,被服務端獲取到,服務端執行具體執行流程,
這時b客戶端連接,只有等待
因爲服務端還沒有處理完A客戶端的請求,還沒有循環回來執行accept方法。所以暫時獲取不到B客戶對象。
那麼爲了可以讓多個客戶端同時併發訪問服務
那麼服務端最好就是將每個客戶端封裝到一個單獨的線程中。這樣,就可以同屬處理多個客戶端請求。
如何定義線程呢?
只要明確了每一個客戶端要在服務端執行的代碼即可。將該代碼存入run方法中。
import java.io.*; import java.net.*; public class PicClient { public static void main(String[] args) throws Exception, IOException { // 主函數傳值 if (args.length != 1) { System.out.println("請選擇一個jpd格式的圖片"); return; } File file = new File(args[0]);//根據傳入的值,實例file對象 if (!(file.exists() && file.isFile())) {//判斷是否存在,是不是文件 System.out.println("該文件有問題,要麼不存在,要麼不是文件"); return; } if (!(file.getName().endsWith(".jpg"))) {//判斷是不是以.jpg爲後綴的 System.out.println("圖片格式錯誤,請重新選擇"); return; } if (file.length() > 1024 * 1024 * 5) {//對文件的大小進行設置 System.out.println("文件過大"); return; } Socket s = new Socket("192.168.1.100", 10004);//指定ip和端口號 FileInputStream fis = new FileInputStream("c:\\1.jpg");//得到文件輸入流,根據文件所在的路徑 OutputStream out = s.getOutputStream();//得到輸出流 byte[] buf = new byte[1024]; int len = 0; while ((len = fis.read(buf)) != -1) {//把文件寫入指定的流中。上傳到客戶端 out.write(buf, 0, len); } // 告訴服務端數據已寫完 s.shutdownOutput(); InputStream in = s.getInputStream();//得到服務端的反饋信息 byte[] bufin = new byte[1024];//緩衝數組 int num = in.read(bufin);//讀取的字節數 System.out.println(new String(bufin, 0, num));//打印輸出 fis.close(); s.close(); } } class PicServer { public static void main(String[] args) throws Exception { ServerSocket ss = new ServerSocket(10004);//服務端Socket指定端口 while (true) { Socket s = ss.accept();//阻塞式接受 new Thread(new PicThread(s)).start();//線程開啓,多線程,併發上傳 } } } class PicThread implements Runnable { private Socket s; PicThread(Socket s) { this.s = s; } public void run() { int count = 1;// 如果定義到成員,多個線程共享數據了。不安全 String ip = s.getInetAddress().getHostAddress(); try { System.out.println(ip + "...connected"); InputStream in = s.getInputStream(); // 爲了不覆蓋,用ip地址來命名 File file = new File(ip + "(" + (count) + ")" + ".jpg"); while (file.exists()) {//判斷文件是否存在 file = new File(ip + "(" + (count++) + ")" + ".jpg"); } FileOutputStream fos = new FileOutputStream(file);//文件輸出流 byte[] buf = new byte[1024]; int len = 0; while ((len = in.read(buf)) != -1) { fos.write(buf, 0, len); } OutputStream out = s.getOutputStream(); out.write("上傳成功".getBytes());//返回給客戶端的信息 fos.close(); s.close(); } catch (Exception e) { throw new RuntimeException(ip + "上傳失敗"); } } }
(9)、TCP客戶端併發登錄
客戶端:
package cn.hwk; import java.io.*; import java.net.*; /* *客戶端通過鍵盤錄入用戶名。 *服務端對這個用戶名進行校驗。 *如果該用戶名存在,在服務端系那是xxx,已登錄 *並在客戶端顯示xxx歡迎光臨。 *如果該用戶不存在在服務端顯示xxx,嘗試登陸。 *並在客戶端顯示xxx,該用戶不存在。 *最多就登陸三次 */ public class LoginClient { public static void main(String[] args) throws Exception, IOException { Socket s = new Socket("192.168.1.102", 10007); BufferedReader bufr = new BufferedReader(new InputStreamReader( System.in)); PrintWriter out = new PrintWriter(s.getOutputStream(), true); BufferedReader bufin = new BufferedReader(new InputStreamReader( s.getInputStream())); for (int x = 0; x < 3; x++) { String line = bufr.readLine();// 讀一次 if (line == null) { break; } out.println(line);// 發出去 String info = bufin.readLine();// 讀取服務端的反饋信息 System.out.println("info:" + info);// 打印反饋信息 if (info.contains("歡迎")) { break; } } bufr.close(); s.close(); } }
服務端:
package cn.hwk; import java.io.*; import java.net.*; public class LoinServer { public static void main(String[] args) throws Exception { ServerSocket ss = new ServerSocket(10007); while (true) { Socket s = ss.accept(); new Thread(new UserThread(s)).start(); } } } class UserThread implements Runnable { private Socket s; UserThread(Socket s) { this.s = s; } public void run() { String ip = s.getInetAddress().getHostAddress(); System.out.println(ip + "...connected"); try { for (int x = 0; x < 3; x++) { BufferedReader bufin = new BufferedReader( new InputStreamReader(s.getInputStream())); String name = bufin.readLine(); if (name == null) {// 客戶端ctrl+c停止了 break; } BufferedReader bufr = new BufferedReader(new FileReader( "c:\\user.txt")); PrintWriter out = new PrintWriter(s.getOutputStream()); String line = null; boolean flag = false; while ((line = bufr.readLine()) != null) { if (line.equals(name)) { flag = true; break; } } if (flag) { System.out.println(name + ",已登錄"); out.println(name + ",歡迎光臨"); break; } else { System.out.println(name + ",嘗試登陸"); out.println(name + ",用戶名不存在"); } } s.close(); } catch (Exception ex) { throw new RuntimeException(ip + "校驗失敗"); } } }