更新時(shí)間:2022-07-20 06:29:12 來源:動(dòng)力節(jié)點(diǎn) 瀏覽1698次
TCP(Transmission Control Protocol),即傳輸控制協(xié)議。是一種面向連接的、可靠的、基于字節(jié)流的傳輸層通信協(xié)議。不同于UDP,TCP更像是提供一種可靠的、像管道一樣的連接。
Java中的TCP主要涉及ServerSocket和Socket兩個(gè)類。前者被認(rèn)為是服務(wù)端的一個(gè)實(shí)體,用于接受連接。后者則被認(rèn)為是連接的一種封裝,用于傳輸數(shù)據(jù),類似于一個(gè)管道。
下面就來實(shí)現(xiàn)一下服務(wù)端與客戶端。
服務(wù)端:
public class TCPService {
public static final String SERVICE_IP = "127.0.0.1";
public static final int SERVICE_PORT = 10101;
public static final char END_CHAR = '#';
public static void main(String[] args) {
TCPService service = new TCPService();
//啟動(dòng)服務(wù)端
service.startService(SERVICE_IP,SERVICE_PORT);
}
private void startService(String serverIP, int serverPort){
try {
//封裝服務(wù)端地址
InetAddress serverAddress = InetAddress.getByName(serverIP);
//建立服務(wù)端
try(ServerSocket service = new ServerSocket(serverPort, 10, serverAddress)){
while (true) {
StringBuilder receiveMsg = new StringBuilder();
//接受一個(gè)連接,該方法會(huì)阻塞程序,直到一個(gè)鏈接到來
try(Socket connect = service.accept()){
//獲得輸入流
InputStream in = connect.getInputStream();
//解析輸入流,遇到終止符結(jié)束,該輸入流來自客戶端
for (int c = in.read(); c != END_CHAR; c = in.read()) {
if(c ==-1)
break;
receiveMsg.append((char)c);
}
//組建響應(yīng)信息
String response = "Hello world " + receiveMsg.toString() + END_CHAR;
//獲取輸入流,并通過向輸出流寫數(shù)據(jù)的方式發(fā)送響應(yīng)
OutputStream out = connect.getOutputStream();
out.write(response.getBytes());
}catch (Exception e){
e.printStackTrace();
}
}
}catch (Exception e){
e.printStackTrace();
}
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
客戶端
public class TCPClient {
public static void main(String[] args) {
TCPClient client = new TCPClient();
SimpleDateFormat format = new SimpleDateFormat("hh-MM-ss");
Scanner scanner = new Scanner(System.in);
while(true){
String msg = scanner.nextLine();
if("#".equals(msg))
break;
//打印響應(yīng)的數(shù)據(jù)
System.out.println("send time : " + format.format(new Date()));
System.out.println(client.sendAndReceive(TCPService.SERVICE_IP,TCPService.SERVICE_PORT,msg));
System.out.println("receive time : " + format.format(new Date()));
}
}
private String sendAndReceive(String ip, int port, String msg){
//這里比較重要,需要給請(qǐng)求信息添加終止符,否則服務(wù)端會(huì)在解析數(shù)據(jù)時(shí),一直等待
msg = msg+TCPService.END_CHAR;
StringBuilder receiveMsg = new StringBuilder();
//開啟一個(gè)鏈接,需要指定地址和端口
try (Socket client = new Socket(ip, port)){
//向輸出流中寫入數(shù)據(jù),傳向服務(wù)端
OutputStream out = client.getOutputStream();
out.write(msg.getBytes());
//從輸入流中解析數(shù)據(jù),輸入流來自服務(wù)端的響應(yīng)
InputStream in = client.getInputStream();
for (int c = in.read(); c != TCPService.END_CHAR; c = in.read()) {
if(c==-1)
break;
receiveMsg.append((char)c);
}
}catch (Exception e){
e.printStackTrace();
}
return receiveMsg.toString();
}
}
單從代碼結(jié)構(gòu)的角度來看,UDP通信服務(wù)端與客戶端代碼是相似的,都是依托于DatagramPacket 對(duì)象收發(fā)信息。而TCP通信中,只有服務(wù)端有一個(gè)實(shí)體,客戶端只要借助Socket收發(fā)信息即可,發(fā)送完關(guān)閉Socket。
上面有一點(diǎn)需要注意,在讀輸入流時(shí),必須做讀到流結(jié)束判斷,就是讀到-1,若沒有做判斷,在這樣情況下會(huì)出錯(cuò):若一個(gè)連接連接成功后,沒有發(fā)生任何信息,或信息中沒有結(jié)束字符,就關(guān)閉了連接,由于TCP連接是雙向的,導(dǎo)致另一端一直從輸入流中讀到流結(jié)束標(biāo)志,很快會(huì)導(dǎo)致OOM,所以在讀到結(jié)束符時(shí),要及時(shí)跳出循環(huán)。結(jié)束符只會(huì)在連接中斷時(shí)發(fā)出,而在等待輸入時(shí),不會(huì)出現(xiàn),所以不必?fù)?dān)心在等待響應(yīng)時(shí)由于讀到該字符導(dǎo)致服務(wù)端或客戶端提前中斷連接。
另外Socket和ServerSocket在jdk 1.7之后都實(shí)現(xiàn)了AutoCloseable接口,所以可以用try-with-resources結(jié)構(gòu)。之前的UDP里的DatagramPacket 也一樣
這就是一個(gè)簡(jiǎn)單的阻塞型服務(wù)器模型,分析代碼我們可知,如果一次請(qǐng)求時(shí)間過長,會(huì)影響到后續(xù)請(qǐng)求的執(zhí)行。我們可以在服務(wù)端輸出時(shí)加一個(gè)sleep,啟動(dòng)兩個(gè)客戶端,分別發(fā)送消息,觀察log,服務(wù)端延遲5s,結(jié)果如下:
客戶端1:
send time : 06-04-06
Hello world 1
receive time : 06-04-11
客戶端2:
send time : 06-04-08
Hello world 2
receive time : 06-04-16
其中客戶端1先發(fā)送,客戶端2后發(fā)送,可見客戶端在等待服務(wù)器處理完客戶端1的請(qǐng)求后才處理客戶端2的請(qǐng)求
由此我們可以預(yù)見,當(dāng)服務(wù)器接到一個(gè)需要長時(shí)間處理的請(qǐng)求時(shí),會(huì)阻塞后續(xù)的請(qǐng)求,這也就是這種類型服務(wù)器容易遭到攻擊的原因。為了應(yīng)對(duì)這種局面,我們可以在收到一個(gè)請(qǐng)求時(shí),調(diào)用子線程去處理,服務(wù)器時(shí)刻處在接受請(qǐng)求的狀態(tài)。
public class TCPService1 {
public static final String SERVICE_IP = "127.0.0.1";
public static final int SERVICE_PORT = 10101;
public static final char END_CHAR = '#';
public static void main(String[] args) {
TCPService1 service1 = new TCPService1();
service1.startService();
}
private void startService(){
try {
InetAddress address = InetAddress.getByName(SERVICE_IP);
Socket connect = null;
ExecutorService pool = Executors.newFixedThreadPool(5);
try (ServerSocket service = new ServerSocket(SERVICE_PORT,5,address)){
while(true){
connect = service.accept();
//創(chuàng)建一個(gè)任務(wù)
ServiceTask serviceTask = new ServiceTask(connect);
//放入線程池等待運(yùn)行
pool.execute(serviceTask);
}
}catch (Exception e){
e.printStackTrace();
}finally {
if(connect!=null)
connect.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
class ServiceTask implements Runnable{
private Socket socket;
ServiceTask(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
StringBuilder receiveMsg = new StringBuilder();
InputStream in = socket.getInputStream();
for (int c = in.read(); c != END_CHAR; c = in.read()) {
if(c ==-1)
break;
receiveMsg.append((char)c);
}
String response = "Hello world " + receiveMsg.toString() + END_CHAR;
Thread.currentThread().sleep(5000);
OutputStream out = socket.getOutputStream();
out.write(response.getBytes());
}catch (Exception e){
e.printStackTrace();
}finally {
if(socket!=null)
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
在這個(gè)服務(wù)器中,我們采用了線程池的做法,每到一個(gè)請(qǐng)求,我們就向線程池中添加一個(gè)任務(wù)。實(shí)際運(yùn)行情況如下:
客戶端1
send time : 03-04-59
Hello world 1
receive time : 03-04-04
客戶端2
send time : 03-04-01
Hello world 2
receive time : 03-04-06
可見每個(gè)客戶端能在發(fā)送信息后得到響應(yīng),不必排隊(duì)。但是這種類型的服務(wù)器并不能保證實(shí)時(shí)響應(yīng),當(dāng)請(qǐng)求數(shù)過多時(shí),服務(wù)器資源會(huì)被耗盡,或者服務(wù)器有最大線程數(shù)有限制,多余的請(qǐng)求依然會(huì)被阻塞。
第一二種服務(wù)器模型中,我們?cè)谧x取流的時(shí)候加入了自定義的結(jié)束符,同時(shí)采用Java for循環(huán),但是一次從輸入流中讀一個(gè)數(shù)據(jù),效率比較低,我們可以采用緩沖區(qū)的方法,但是這種方法不能判斷自定義的結(jié)束符,只能判斷流結(jié)束,所以要及時(shí)關(guān)閉流,如客戶端發(fā)完數(shù)據(jù)后關(guān)閉輸出流:
OutputStream out = client.getOutputStream();
out.write(msg.getBytes());
client.shutdownOutput();
InputStream in = client.getInputStream();
int len;
byte[] buffer = new byte[1024];
while((len = in.read(buffer))!=-1)
receiveMsg.append(new String(buffer,0,len));
由于TCP通信是雙向的,所以可以單獨(dú)關(guān)閉一端,但是不能直接關(guān)閉輸入或輸出流,這樣會(huì)將整個(gè)Socket關(guān)閉。
相關(guān)閱讀
0基礎(chǔ) 0學(xué)費(fèi) 15天面授
有基礎(chǔ) 直達(dá)就業(yè)
業(yè)余時(shí)間 高薪轉(zhuǎn)行
工作1~3年,加薪神器
工作3~5年,晉升架構(gòu)
提交申請(qǐng)后,顧問老師會(huì)電話與您溝通安排學(xué)習(xí)
初級(jí) 202925
初級(jí) 203221
初級(jí) 202629
初級(jí) 203743