網絡程式設計
軟體結構
C/S結構 :全稱為Client/Server結構,是指用戶端和伺服器結構。
B/S結構 :全稱為Browser/Server結構,是指浏覽器和伺服器結構。
網絡通信協定
網絡通信協定:位于同一個網絡中的計算機在進行連接配接和通信時需要遵守一定的規則,它對資料的傳輸格式、傳輸速率、傳輸步驟等做了統一規定
TCP/IP協定:它定義了計算機如何連入網際網路,以及資料如何在它們之間傳輸的标準。它的内部包含一系列的用于處理資料通信的協定,每一層都呼叫它的下一層所提供的協定來完成自己的需求

- 應用層:網絡服務與最終使用者的一個接口。協定有:HTTP、FTP、SMTP、DNS、TELNET、HTTPS、POP3等等。
- 表示層:資料的表示、安全、壓縮。格式有:JPEG、ASCll、DECOIC、加密格式等。
- 會話層:建立、管理、終止會話。對應主機程序,指本地主機與遠端主機正在進行的會話
- 傳輸層:定義傳輸資料的協定端口号,以及流控和差錯校驗。協定有:TCP、UDP。
- 網絡層:進行邏輯位址尋址,實作不同網絡之間的路徑選擇。協定有:ICMP、IGMP、IP(IPV4 IPV6)、ARP、RARP。
- 資料鍊路層:建立邏輯連接配接、進行硬體位址尋址、差錯校驗等功能。将比特組合成位元組進而組合成幀,用MAC位址通路媒體,錯誤發現但不能糾正。
- 實體層:建立、維護、斷開實體連接配接。
IP(internet protocal)又稱為網際網路協定。IP的責任就是把資料從源傳送到目的地。它在源位址和目的位址之間傳送一種稱之為資料包的東西,它還提供對資料大小的重新組裝功能,以适應不同網絡對包大小的要求。經常與IP協定放在一起的還有TCP(Transmission Control Protocol)協定
TCP與UDP協定
UDP:使用者資料報協定(User Datagram Protocol)。
- 非面向連接配接的,不可靠的:發送端不會确認接收端是否存在,就會發出資料,同樣接收端在收到資料時,也不會向發送端回報是否收到資料。
- 大小限制的:資料被限制在64kb以内,超出這個範圍就不能發送了。
- 資料報(Datagram):網絡傳輸的基本機關
TCP:傳輸控制協定 (Transmission Control Protocol)。
- 面向連接配接的,可靠的:在發送端和接收端建立邏輯連接配接,然後再傳輸資料,是一種面向連接配接的、可靠的、基于位元組流的傳輸層的通信協定,可以連續傳輸大量的資料。TCP協定保證了資料包在傳送中準确無誤,TCP協定使用重發機制,需要收到另一個通信實體的确認資訊
- 三次握手
- 四次揮手
網絡程式設計三要素
1、協定:如上
2、IP位址
IP位址用來給一個網絡中的計算機裝置做唯一的編号,
IPv4:32位整數,8位一組最多可以表示42億個
IPv6:采用128位位址長度,每16個位元組一組,分成8組十六進制數
公網位址( 網際網路使用)和 私有位址( 區域網路使用)。192.168.開頭的就是私有址址,範圍即為192.168.0.0--192.168.255.255,專門為組織機構内部使用
特殊的IP位址:
- 本地回環位址(hostAddress):
127.0.0.1
- 主機名(hostName):
localhost
域名:域名伺服器(DNS)負責将域名轉化成IP位址,友善記憶。
3、端口号
端口号可以找到唯一辨別裝置中的程序(應用程式)0~65535,動态/ 私有端口:49152~65535
如果端口号被另外一個服務或應用所占用,會導緻目前程式啟動失敗。
利用
協定
+
IP位址
端口号
三元組合,就可以辨別網絡中的程序了,那麼程序間的通信就可以利用這個辨別與其它程序進行互動。
InetAddress類
InetAddress類主要表示IP位址,兩個子類:Inet4Address、Inet6Address。
lInetAddress 類沒有提供公共的構造器,而是提供 了 如下幾個 靜态方法來擷取InetAddress 執行個體
- public static InetAddress getLocalHost() 【傳回本地主機】
- public static InetAddress getByName(String host) 【在給定主機名的情況下确定主機的 IP 位址】
- public static InetAddress getByAddress(byte[] addr) 【在給定原始 IP 位址的情況下,傳回
對象】InetAddress
- 例:byte[] addr = {(byte)192,(byte)168,24,56}; 其内部用一個int存儲
InetAddress 提供了如下幾個常用的方法
- public String getHostAddress() : 【傳回 IP 位址字元串(以文本表現形式)】
- public String getHostName() : 【擷取此 IP 位址的主機名】
- public String getCanonicalHostName(): 【擷取此 IP 位址的完全限定域名】
- boolean isReachable(int timeout) 【測試是否可以達到該位址。】
Socket
socket 可了解為一個介于應用層與協定層之間的一個抽象層,它屏蔽了各個協定的通信細節,使得程式員無需關注協定本身,直接使用socket提供的接口來進行互聯的不同主機間的程序的通信。
就是提供了tcp/ip協定的抽象,對外提供了一套接口,通過這個接口就可以統一、友善的使用tcp/ip協定的功能了
通信的兩端都要有Socket(也可以叫“套接字”),是兩台機器間通信的端點。網絡通信其實就是Socket間的通信。也是負責和網卡驅動程式溝通的對象。socket工作流程
socket是兩個主機通信的關鍵,先了解IO流的工作流程有助于了解網絡間的通信,socket資料的發送與接收也可簡單的了解為:
用戶端将要發送的資料通過send()發送給用戶端的tcp/udp協定的緩沖區,由用戶端協定發送給服務端的tcp/udp協定,服務端的receive()會讀取服務端的協定緩沖區接收到的資料,
如需傳回資料再經服務端的send()發送給服務端的協定緩沖區,服務端的協定再發送給用戶端的協定,用戶端的receive()會讀取用戶端協定緩沖區中的資料如此循環,直到close()
send()與receive()函數
- ServerSocket:此類實作TCP伺服器套接字。伺服器套接字等待請求通過網絡傳入。流套接字
- Socket:此類實作用戶端套接字(也可以就叫“套接字”)。套接字是兩台機器間通信的端點。流套接字
- DatagramSocket:此類表示用來發送和接收UDP資料報包的套接字。資料報套接字
TCP網絡程式設計
1、伺服器端
- 調用 ServerSocket(int port) :建立一個伺服器端套接字,并綁定到指定端口上。用于監聽用戶端的請求。
- 調用 accept() :監聽連接配接請求,如果用戶端請求連接配接,則接受連接配接,傳回通信套接字對象。
- 調用 該Socket 類對象的 getOutputStream() 和 getInputStream () :擷取輸出流和輸入流,開始網絡資料的發送和接收。
- 關閉Socket 對象:用戶端通路結束,關閉通信套接字。
2、用戶端
- 建立 Socket:根據指定服務端的 IP 位址或端口号構造 Socket 類對象。若伺服器端響應,則建立用戶端到伺服器的通信線路。若連接配接失敗,會出現異常。
- 打開連接配接到 Socket 的輸入/出流: 使用 getInputStream()方法獲得輸入流,使用getOutputStream()方法獲得輸出流,進行資料傳輸
- 按照一定的協定對 Socket 進行讀/ 寫操作:通過輸入流讀取伺服器放入線路的資訊(但不能讀取自己放入線路的資訊),通過輸出流将資訊寫入線路。
- 關閉 Socket:斷開用戶端到伺服器的連接配接,釋放線路
API
ServerSocket類的構造方法:
- ServerSocket(int port) : 【建立綁定到特定端口的伺服器套接字】
ServerSocket類的常用方法:
- Socket accept() 【偵聽并接受到此套接字的連接配接。】
- InetAddress getInetAddress() 【傳回此伺服器套接字的本地位址。】
- int getLocalPort() 【傳回此套接字在其上偵聽的端口。】
- void close(): 【關閉此套接字。】
Socket類的常用構造方法:
- public Socket(InetAddress address,int port): 【建立一個流套接字并将其連接配接到指定 IP 位址的指定端口号】
- public Socket(String host,int port): 【建立一個流套接字并将其連接配接到指定主機上的指定端口号】
Socket類的常用方法:
- public InputStream getInputStream(): 【傳回此套接字的輸入流,可以用于接收消息】
- public OutputStream getOutputStream(): 【傳回此套接字的輸出流,可以用于發送消息】
- public InetAddress getInetAddress(): 【傳回此套接字連接配接到的遠端 IP 位址;如果套接字是未連接配接的,則傳回 null】
- public InetAddress getLocalAddress(): 【擷取套接字綁定的本地位址】
- public int getPort(): 【傳回此套接字連接配接到的遠端端口号;如果尚未連接配接套接字,則傳回 0】
- public int getLocalPort(): 【傳回此套接字綁定到的本地端口。如果尚未綁定套接字,則傳回 -1】
- public void close(): 【關閉套接字(即無法重新連接配接或重新綁定) 同時也将會關閉該套接字的 InputStream 和 OutputStream】
- public void shutdownInput():
如果在套接字上調用 shutdownInput() 後從套接字輸入流讀取内容,則流将傳回 EOF(檔案結束符)。 即不能在從此套接字的輸入流中接收任何資料。關閉輸入流
- public void shutdownOutput():
禁用此套接字的輸出流。對于 TCP 套接字,任何以前寫入的資料都将被發送,并且後跟 TCP 的正常連接配接終止序列。 如果在套接字上調用 shutdownOutput() 後寫入套接字輸出流,則該流将抛出 IOException。 即不能通過此套接字的輸出流發送任何資料。關閉輸出流
- boolean isInputShutdown() : 【傳回是否關閉套接字連接配接的半讀狀态 (read-half)】
- boolean isOutputShutdown() : 【傳回是否關閉套接字連接配接的半寫狀态 (write-half)】
注意:先後調用Socket的shutdownInput()和shutdownOutput()方法,僅僅關閉了輸入流和輸出流,并不等于調用Socket的close()方法。在通信結束後,仍然要調用Scoket的close()方法,因為隻有該方法才會釋放Socket占用的資源,比如占用的本地端口号等。
如果伺服器端要“同時”處理多個用戶端的請求,是以伺服器端需要為每一個用戶端單獨配置設定一個線程來處理,否則無法實作“同時”。
UDP網絡程式設計
UDP(User Datagram Protocol,使用者資料報協定)特點:
在正式通信前不必與對方先建立連接配接,至于對方是否可以接收到這些資料内容,UDP協定無法控制,無連接配接的好處就是快,省記憶體空間和流量,沒有TCP的确認機制、重傳機制,如果因為網絡原因沒有傳送到對端,UDP也不會給應用層傳回錯誤資訊。
UDP協定是面向資料封包的資訊傳送服務。UDP在發送端沒有緩沖區,對于應用層傳遞下來的封包在添加了首部之後就直接傳遞于ip層,不會進行合并,也不會進行拆分,而是一次傳遞一個完整的封包。
UDP協定沒有擁塞控制,是以當網絡出現的擁塞不會導緻主機發送資料的速率降低。雖然UDP的接收端有緩沖區,但是這個緩沖區隻負責接收,并不會保證UDP封包的到達順序是否和發送的順序一緻。
是以UDP适用于一次隻傳送少量資料、對可靠性要求不高的應用環境,資料報大小限制在64K以下。
基于UDP協定的網絡程式設計仍然需要在通信執行個體的兩端各建立一個Socket,但這兩個Socket之間并沒有虛拟鍊路,這兩個Socket隻是發送、接收資料報的對象
DatagramSocket 類的常用方法:
- public DatagramSocket(int port)
建立資料報套接字并将其綁定到本地主機上的指定端口。套接字将被綁定到通配符位址,IP 位址由核心來選擇。
- public DatagramSocket(int port,InetAddress laddr)
建立資料報套接字,将其綁定到指定的本地位址。本地端口必須在 0 到 65535 之間(包括兩者)。如果 IP 位址為 0.0.0.0,套接字将被綁定到通配符位址,IP 位址由核心選擇。
- public void send(DatagramPacket p)
從此套接字發送資料報包。DatagramPacket 包含的資訊訓示:将要發送的資料、其長度、遠端主機的 IP 位址和遠端主機的端口号。
- public void receive(DatagramPacket p)
從此套接字接收資料報包。當此方法傳回時,DatagramPacket 的緩沖區填充了接收的資料。資料報包也包含發送方的 IP 位址和發送方機器上的端口号。 此方法在接收到資料報前一直阻塞。資料報包對象的 length 字段包含所接收資訊的長度。如果資訊比包的長度長,該資訊将被截短。
- public void close() 【關閉此資料報套接字。】
DatagramPacket類的常用方法:
- public DatagramPacket(byte[] buf,int length)
構造 DatagramPacket,用來接收長度為 length 的資料包。 length 參數必須小于等于 buf.length。
- public DatagramPacket(byte[] buf,int length,InetAddress address,int port)
構造資料報包,用來将長度為 length 的包發送到指定主機上的指定端口号。length 參數必須小于等于 buf.length。
- public int getLength() 【傳回将要發送或接收到的資料的長度】
多點廣播
Datagram隻允許資料報發送給指定的目标位址,而MulticastSocket可以将資料報以廣播方式發送到數量不等的多個用戶端。
IP協定為多點廣播提供了這批特殊的IP位址,這些IP位址的範圍是224.0.0.0至239.255.255.255。
MulticastSocket常用的方法:
- MulticastSocket(int port) :
建立多點傳播套接字并将其綁定到特定端口。建立一個MulticastSocket對象後,還需要将該MulticastSocket加入到指定的多點廣播位址,如果結束也需要脫離多點廣播位址。
- void joinGroup(InetAddress mcastaddr) :【加入多點傳播組。】
- void leaveGroup(InetAddress mcastaddr) :【離開多點傳播組。】
- void setLoopbackMode(boolean disable) :【啟用/禁用多點傳播資料報的本地回送。true 表示禁用LoopbackMode。】
TCP網絡程式設計示例:群聊
用戶端未導包
public class Client {
public static void main(String[] args) throws UnknownHostException, IOException {
// 1、連接配接伺服器
Socket socket = new Socket("127.0.0.1", 9999);
// 2、開啟兩個線程,一個收消息,一個發消息
SendThread st = new SendThread(socket);
ReceiveThread rt = new ReceiveThread(socket);
st.start();
rt.start();
// 等發送線程停下來再往下走
try {
st.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 等接收線程停下來,再往下走,斷開連接配接
try {
rt.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
socket.close();
}
static class SendThread extends Thread {
private Socket socket;
public SendThread(Socket socket) {
super();
this.socket = socket;
}
public void run() {
try {
// 鍵盤輸入
Scanner input = new Scanner(System.in);
OutputStream out = socket.getOutputStream();
PrintStream ps = new PrintStream(out);
while (true) {
// 從鍵盤輸入
System.out.print("請輸入要發送的消息:");
String content = input.nextLine();
// 給伺服器發送
ps.println(content);
// 如果bye,就結束發送
if ("bye".equals(content)) {
break;
}
}
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
static class ReceiveThread extends Thread {
private Socket socket;
public ReceiveThread(Socket socket) {
super();
this.socket = socket;
}
public void run() {
try {
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in);
BufferedReader br = new BufferedReader(isr);
while (true) {
String line = br.readLine();
if("bye".equals(line)){
break;
}
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
伺服器端
public class Server {
private static ArrayList<Socket> online = new ArrayList<Socket>();
public static void main(String[] args) throws IOException {
//1、開啟伺服器
ServerSocket server = new ServerSocket(9999);
while(true){
//2、接收用戶端的連接配接
Socket socket = server.accept();
//把這個用戶端加入到online中
online.add(socket);
//每一個用戶端獨立的線程
MessageHandler mh = new MessageHandler(socket);
mh.start();
}
}
private static class MessageHandler extends Thread{
private Socket socket;
private String ip;
public MessageHandler(Socket socket) {
super();
this.socket = socket;
this.ip = socket.getInetAddress().getHostAddress();
}
public void run(){
//這個用戶端的一連接配接成功,線程一啟動,就可以告訴其他人我上線了
sendToOthers(ip+"上線了");
//(1)接收目前的用戶端發送的消息
try {
InputStream in = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(in);
BufferedReader br = new BufferedReader(isr);
String content;
while((content = br.readLine()) !=null){
//收到一句,轉發一句
sendToOthers(ip+"說:" + content);
if("bye".equals(content)){
//給自己發一句bye
OutputStream out = socket.getOutputStream();
PrintStream ps = new PrintStream(out);
ps.println("bye");
break;
}
}
sendToOthers(ip+"下線了");
} catch (IOException e) {
sendToOthers(ip+"掉線了");
}
}
//因為轉發的代碼也很長,獨立為一個方法
public void sendToOthers(String str){
//周遊所有online的用戶端
Iterator<Socket> iterator = online.iterator();
while(iterator.hasNext()){
Socket on = iterator.next();
if(!on.equals(socket)){//隻給其他用戶端轉發
try {
OutputStream out = on.getOutputStream();
PrintStream ps = new PrintStream(out);
ps.println(str);
} catch (IOException e) {
//說明on這個用戶端要麼下線了,要麼掉線了
iterator.remove();
}
}
}
}
}
}