論壇門戶網(wǎng)站建設(shè)seo文章外包
概念
在網(wǎng)絡(luò)編程中主要的對(duì)象有兩個(gè):客戶端和服務(wù)器??蛻舳耸翘峁┱?qǐng)求的,歸用戶使用,發(fā)送的請(qǐng)求會(huì)被服務(wù)器接收,服務(wù)器根據(jù)請(qǐng)求做出響應(yīng),然后再將響應(yīng)的數(shù)據(jù)包返回給客戶端。
作為程序員,我們主要關(guān)心應(yīng)用層和傳輸層,我們編寫的程序?qū)儆趹?yīng)用層,需要調(diào)用傳輸層的接口來進(jìn)行數(shù)據(jù)的傳輸。Java給我們提供了兩套接口,一套是屬于UDP 協(xié)議的,另一套是屬于 TCP 協(xié)議的。本篇文章重點(diǎn)講解UDP 數(shù)據(jù)報(bào)套接字編程。
Socket套接字,是由系統(tǒng)提供用于網(wǎng)絡(luò)通信的技術(shù),是基于TCP/IP協(xié)議的網(wǎng)絡(luò)通信的基本操作單元。
基于Socket套接字的網(wǎng)絡(luò)程序開發(fā)就是網(wǎng)絡(luò)編程。
UDP數(shù)據(jù)報(bào)套接字
DatagramSocket
DatagramSocket 簡(jiǎn)單來理解就是定位你所在的位置,用于接收和發(fā)送數(shù)據(jù)報(bào)
構(gòu)造方法:
方法名 | 說明 |
---|---|
DatagramSocket() | 無參構(gòu)造方法,不指定端口號(hào),由操作系統(tǒng)自行分配 |
DatagramSocket(int port) | port 就是端口號(hào),這個(gè)構(gòu)造方法就是由程序員自行指定端口號(hào) |
接收和發(fā)送數(shù)據(jù)包的方法:
方法名 | 說明 |
---|---|
send(DatagramPacket p) | 發(fā)送數(shù)據(jù)包 |
receive(DatagramPacket p | 接收數(shù)據(jù)包,這里是輸出型參數(shù),傳輸層把數(shù)據(jù)內(nèi)容填充到你傳入的數(shù)據(jù)包中 |
什么是輸出型參數(shù)?
該參數(shù)在方法內(nèi)部會(huì)被改變,并且會(huì)影響到方法外部的實(shí)參。
關(guān)閉方法:
方法名 | 說明 |
---|---|
close() | 關(guān)閉資源 |
注意了網(wǎng)絡(luò)編程使用 Socket ,也是和內(nèi)存、文件一樣都會(huì)消耗資源的。
DatagramPacket
DatagramPacket 就是數(shù)據(jù)報(bào),也就是你發(fā)送和接收的數(shù)據(jù)報(bào),這里數(shù)據(jù)報(bào)和數(shù)據(jù)包不作區(qū)分,大家知道就好了。
構(gòu)造方法:
字節(jié)數(shù)組就是用來填充數(shù)據(jù)的,也是輸出型參數(shù)。
offset 是指定偏移量
length 是指定要填充多少個(gè)字節(jié)
address 就是傳入地址,SocketAddress 就是一個(gè)完整的地址(包含IP 和端口號(hào)),InetAddress 只是包含 IP地址,port 就是我們熟悉的端口號(hào)。
其他方法:
方法名 | 說明 | 返回值 |
---|---|---|
getAddress() | 獲得該數(shù)據(jù)包的 IP 地址 | InetAddress |
getPort() | 獲得該數(shù)據(jù)包端口號(hào) | int |
getSocketAddress() | 獲得該數(shù)據(jù)包的完整地址(包含IP地址和端口號(hào)) | SocketAddress |
getData() | 或者數(shù)據(jù)內(nèi)容 | byte[] |
getLength() | 獲得數(shù)據(jù)的長(zhǎng)度,以字節(jié)為單位 | int |
InetSocketAddress
構(gòu)造方法:
方法名 | 說明 |
---|---|
InetSocketAddress(InetAddress addr, int port) | 創(chuàng)建一個(gè) Socket 地址,包含IP地址和端口號(hào) |
其他方法:
方法名 | 說明 | 注意 |
---|---|---|
getByName(String host) | 將主機(jī)名轉(zhuǎn)化為機(jī)器能識(shí)別的IP地址 | 靜態(tài)方法 |
這個(gè)方法有什么用?
我們知道一個(gè)IP地址我們習(xí)慣用十進(jìn)制來表示,類似”xxx.xxx.xxx.xxx",我們通常傳入的這個(gè)IP地址是一個(gè)字符串,這個(gè)方法就能將這個(gè)字符串轉(zhuǎn)化為機(jī)器能識(shí)別的二進(jìn)制的 IP 地址。
回顯服務(wù)器編寫
這里簡(jiǎn)單介紹一下,回顯服務(wù)器就是你發(fā)什么我就回什么,例如客戶端發(fā)送一個(gè) hello,服務(wù)器直接返回 hello,這就是回顯服務(wù)器,此服務(wù)器是用來我們學(xué)習(xí)套接字的?,F(xiàn)在我來帶領(lǐng)大家完成服務(wù)器代碼的編寫。
首先創(chuàng)建一個(gè)服務(wù)器的類,這里定義為 UdpEchoServer,Echo 就是回顯的意思。
在類里面先定義字段 DatagramSocket socket
private DatagramSocket socket;//給服務(wù)器指定一個(gè)端口號(hào)public UdpEchoServer(int port) throws SocketException {socket = new DatagramSocket(port);}
在構(gòu)造方法這里要指定對(duì)應(yīng)的端口號(hào),服務(wù)器的位置一定要固定下來,防止客戶端那邊找不到服務(wù)器。
啟動(dòng)程序
服務(wù)器是 7 * 24 小時(shí)為用戶提供的服務(wù)的,所以這里我們直接寫一個(gè)死循環(huán) while(true) {}
每一次循環(huán)都是在處理一次請(qǐng)求。
首先我們要接收客戶端的數(shù)據(jù)包,先創(chuàng)建好一個(gè)空的數(shù)據(jù)包來接收數(shù)據(jù),這里為什么不傳入地址,因?yàn)槲覀冞@個(gè)數(shù)據(jù)包只是用來接收數(shù)據(jù)的,并且就在服務(wù)器中使用,不需要添加地址。
//構(gòu)建請(qǐng)求數(shù)據(jù)包
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
然后接收:
//獲取請(qǐng)求
socket.receive(requestPacket);//輸出型參數(shù)
如果沒有數(shù)據(jù)可以接收的話,服務(wù)器程序會(huì)一直在這里阻塞住。
解析數(shù)據(jù)包并計(jì)算請(qǐng)求,由于這里是回顯服務(wù)器,所以我們直接構(gòu)造出 String,然后形式上進(jìn)行響應(yīng)的處理:
//解析請(qǐng)求數(shù)據(jù)包
String request = new String(requestPacket.getData(), 0, requestPacket.getLength());//計(jì)算響應(yīng)值
//這里是回顯服務(wù)器,直接返回原數(shù)據(jù)
String response = process(request);
//計(jì)算響應(yīng),服務(wù)器的核心代碼區(qū)域private String process(String request) {return request;}
在真實(shí)的服務(wù)器代碼中,我們?cè)陧憫?yīng)這里的處理是服務(wù)器的核心邏輯,由于是回顯服務(wù)器,也就顯得沒有什么感受。
之后就要把響應(yīng)的數(shù)據(jù)包發(fā)送回客戶端那邊。
注意:由于 UDP 是不會(huì)保存對(duì)端的 IP地址和端口號(hào)的,所以我們?cè)跇?gòu)建響應(yīng)數(shù)據(jù)包的時(shí)候,一定要傳入目的 IP 和 目的端口號(hào),這里的目的 IP 和 目的端口號(hào)可以從請(qǐng)求的數(shù)據(jù)包獲取,因?yàn)檎?qǐng)求的數(shù)據(jù)包保存了客戶端的IP 和 端口號(hào)。
//構(gòu)建響應(yīng)數(shù)據(jù)包
//UDP 不存放對(duì)端的源 IP 和 源端口,所以需要傳入對(duì)方的地址
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
requestPacket.getSocketAddress());//發(fā)送響應(yīng)數(shù)據(jù)包
socket.send(responsePacket);
還要注意數(shù)據(jù)的長(zhǎng)度一定是response.getBytes().length
,不要寫出字符串的長(zhǎng)度,因?yàn)槲覀兊臄?shù)據(jù)是字節(jié),字節(jié)的大小和字符的大小是不一樣的。
最后我們可以打印一個(gè)日志:
//打印日志
System.out.printf("[%s : %d] request: %s response: %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);
因?yàn)?getAddress() 的返回值是 InetAddress ,所以要使用 toString() 轉(zhuǎn)化為字符串進(jìn)行打印。
最終代碼
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;//服務(wù)器程序
public class UdpEchoServer {private DatagramSocket socket;//給服務(wù)器指定一個(gè)端口號(hào)public UdpEchoServer(int port) throws SocketException {socket = new DatagramSocket(port);}//服務(wù)器啟動(dòng)運(yùn)行程序public void start() throws IOException {System.out.println("服務(wù)器啟動(dòng)...");//服務(wù)器持續(xù)運(yùn)行//每次循環(huán)處理一次請(qǐng)求while(true) {//構(gòu)建請(qǐng)求數(shù)據(jù)包DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);//獲取請(qǐng)求socket.receive(requestPacket);//輸出型參數(shù)//解析請(qǐng)求數(shù)據(jù)包String request = new String(requestPacket.getData(), 0, requestPacket.getLength());//計(jì)算響應(yīng)值//這里是回顯服務(wù)器,直接返回原數(shù)據(jù)String response = process(request);//構(gòu)建響應(yīng)數(shù)據(jù)包//UDP 不存放對(duì)端的源IP 和 源端口,所以需要傳入對(duì)方的地址DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,requestPacket.getSocketAddress());//發(fā)送響應(yīng)數(shù)據(jù)包socket.send(responsePacket);//打印日志System.out.printf("[%s : %d] request: %s response: %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);}}//計(jì)算響應(yīng),服務(wù)器的核心代碼區(qū)域private String process(String request) {return request;}public static void main(String[] args) throws IOException {UdpEchoServer server = new UdpEchoServer(9090);server.start();}
}
客戶端編寫
客戶端是一定要知道請(qǐng)求是發(fā)到哪一個(gè)服務(wù)器上的,所以我們需要保存好服務(wù)器的IP 地址和端口號(hào)。
private DatagramSocket socket;
private String serverIP;
private int serverPort;
構(gòu)造方法:注意一定要傳入服務(wù)器的 IP地址 和 端口號(hào),socket 使用的是 DatagramScoket 的無參構(gòu)造方法。
為什么使用的是 DatagramScoket 的無參構(gòu)造方法?
首先我們作為程序員不知道用戶那邊的主機(jī)的端口使用情況,如果固定用戶的端口號(hào),正好用戶此時(shí)已經(jīng)有進(jìn)程占用了這個(gè)端口號(hào),這時(shí)候我們的客戶端程序是跑不起來的,這就是端口沖突。
為了避免端口的沖突,我們不指定端口號(hào),而是交給用戶主機(jī)的操作系統(tǒng)自行指定端口號(hào)。
public UdpEchoClient(String serverIP, int serverPort) throws SocketException {this.serverPort = serverPort;this.serverIP = serverIP;socket = new DatagramSocket();//不用指定客戶端的端口號(hào),讓用戶自己的操作系統(tǒng)自己去安排端口號(hào),避免端口號(hào)沖突}
啟動(dòng)程序
這里我們直接讓用戶從控制臺(tái)輸入要發(fā)送的數(shù)據(jù),我們構(gòu)建好請(qǐng)求數(shù)據(jù)包并發(fā)送到服務(wù)器上
//構(gòu)建請(qǐng)求數(shù)據(jù)包
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIP),serverPort);//發(fā)送數(shù)據(jù)包
socket.send(requestPacket);
這里我們就使用了InetAddress.getByName()
這個(gè)方法將本身是 String 類型的IP地址轉(zhuǎn)化為機(jī)器能識(shí)別的IP地址。
然后要注意數(shù)據(jù)的長(zhǎng)度一定是request.getBytes().length
,不要寫出字符串的長(zhǎng)度,因?yàn)槲覀兊臄?shù)據(jù)是字節(jié),字節(jié)的大小和字符的大小是不一樣的。
最后就是傳入目的地址也就是服務(wù)器的源IP和源端口號(hào),然后發(fā)送給服務(wù)器那邊。
接著就是接收服務(wù)器的響應(yīng),我們構(gòu)建一個(gè)空的響應(yīng)數(shù)據(jù)包來接收:
//接收數(shù)據(jù)包
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
最后就是解析并打印響應(yīng)內(nèi)容了。
//打印響應(yīng)數(shù)據(jù)
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
System.out.println("響應(yīng):" + response);
最終代碼
import java.io.IOException;
import java.net.*;
import java.util.Scanner;//客戶端程序
public class UdpEchoClient {private DatagramSocket socket;private String serverIP;private int serverPort;public UdpEchoClient(String serverIP, int serverPort) throws SocketException {this.serverPort = serverPort;this.serverIP = serverIP;socket = new DatagramSocket();//不用指定客戶端的端口號(hào),讓用戶自己的操作系統(tǒng)自己去安排端口號(hào),避免端口號(hào)沖突}public void start() throws IOException {System.out.println("歡迎來到客戶端...");Scanner scan = new Scanner(System.in);while(true) {//用戶從控制臺(tái)輸入數(shù)據(jù)System.out.println("請(qǐng)輸入你要發(fā)送的數(shù)據(jù):");while(!scan.hasNext()) {break;}String request = scan.nextLine();//構(gòu)建請(qǐng)求數(shù)據(jù)包DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(serverIP),serverPort);//發(fā)送數(shù)據(jù)包socket.send(requestPacket);//接收數(shù)據(jù)包DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);socket.receive(responsePacket);//打印響應(yīng)數(shù)據(jù)String response = new String(responsePacket.getData(),0,responsePacket.getLength());System.out.println("響應(yīng):" + response);}}public static void main(String[] args) throws IOException {UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);client.start();}
}
由于我這里是使用一臺(tái)主機(jī)的兩個(gè)進(jìn)程來模擬客戶端和服務(wù)器的,所以IP地址指定為"127.0.0.1",這是每臺(tái)主機(jī)默認(rèn)的IP地址,端口號(hào)這里指定為 9090,最后大家運(yùn)行兩個(gè)程序,就可以看到下面的效果了。
效果展示: