网络编程(3)-----------Javaweb
咱們既然要寫一個TCP版本的客戶端服務(wù)器,我們要建立連接TCP三次握手(建立連接):
1)客戶端:
Socket對象
我們直接通過創(chuàng)建Socket對象,把服務(wù)器的IP地址和端口號傳入進(jìn)去,這樣就模擬了三次握手
2)服務(wù)器:
ServerSocket(房屋中介)
Socket(小姐姐)
我們在服務(wù)器直接調(diào)用ServerSocket的accept方法建立連接,沒有方法建立連接,那么就會阻塞
? ? ? ? ?
2)下面我們來寫一下 TCP網(wǎng)絡(luò)編程(下面是我們需要注意的一些點)
服務(wù)器流程:
1)第一個類:ServerSocket,創(chuàng)建的是實例是listensocket,需要給listenSocket關(guān)聯(lián)上一個端口號,他的構(gòu)造方法是ServerSocket(int port);
2)他的其中的accept這樣的方法和TCP有連接有著很大的關(guān)系,accept就相當(dāng)于接電話這個操作,監(jiān)聽指定端口,相當(dāng)于在外場拉客(買房子在場外拉客,來和我們的業(yè)務(wù)人員溝通一下買房唄),把一個內(nèi)核建立好的鏈接交給Socket代碼來處理
listenSocket是ServerSocket創(chuàng)建的實例,它主要的作用是:
1)調(diào)用accept方法,是用來與另一臺主機(jī)進(jìn)行連接,確定是有連接的,如果主機(jī)一直無法建立連接,就會出現(xiàn)阻塞
2)把主機(jī)傳輸過來的數(shù)據(jù)交給我們的climentSocket進(jìn)行處理,場外拉客的人直接把客戶交給業(yè)務(wù)人員
也就是說客戶端嘗試建立連接,首先是服務(wù)器的操作系統(tǒng)這一層和客戶端進(jìn)行一些相關(guān)的流程,先把這個連接建立好,用戶代碼調(diào)用accept才真正把這個鏈接放到用戶代碼中
3)第二個類 Socket,在服務(wù)器收到客戶端建立連接后,返回的服務(wù)器端Socket,它主要是在內(nèi)場給客戶端提供具體的服務(wù),把一臺主機(jī)傳輸過來的數(shù)據(jù)進(jìn)行解析,并進(jìn)行返回
4)再使用climentsocket的getInputStream和getoutputStream對象得到一個字節(jié)流對象,這時就可以進(jìn)行讀取和寫入了;(此時的讀操作可以調(diào)用inputstream.read()里面?zhèn)魅胍粋€字節(jié)數(shù)組,然后再轉(zhuǎn)化成String,但是比較麻煩),我們使用inputStream.read()的時候,就相當(dāng)于是讀取客戶端發(fā)送過來的數(shù)據(jù)
5)我們可以直接通過climentSocket.getInputStream()來進(jìn)行獲取到流對象,但是具體讀的時候Scanner 來進(jìn)行具體的讀,new Scanner(InputStreaam),這事就巧妙地讀到了客戶端的請求,在調(diào)用scan.next();寫的時候,直接利用Printstream new Printstream()構(gòu)造方法里面直接寫Outputstream,當(dāng)調(diào)用println方法的時候,就默認(rèn)寫回到客戶端了,當(dāng)我們使用outputStream.write()方法的時候,就相當(dāng)于向我們的客戶端返回了數(shù)據(jù)
一:比如說我以后想去買房,剛出門迎面就走來了一個西裝革履的小哥哥,這個小哥哥就帶著我來到了樓盤售樓部,這里面是人山人海,這個小哥哥進(jìn)去之后就打了聲招呼,喊出了一個年輕的小姐姐
二:這個小哥哥就說:這個小姐姐是咱們這個樓盤里面的置業(yè)顧問,由它來給你介紹這個該樓盤的銷售情況,然后這個小哥哥就走了,他會繼續(xù)回到這個馬路上面,回到馬路牙子上,繼續(xù)去拉其他的人
三:然后我就和這個小姐姐不斷地進(jìn)行交流,由此可見小哥哥就是listenSocke(只有一個人),小姐姐(多個)就是climentSocket,買房的人就是一個客戶端(數(shù)據(jù)),對于咱們的每一個想要買房的人,都需要自動分配一個climentSocket(小姐姐)
所以這里面就分成了兩步,一個是專門負(fù)責(zé)數(shù)據(jù)連接,一個是專門負(fù)責(zé)數(shù)據(jù)通信
客戶端過程:全程只需要使用Socket對象
1)創(chuàng)建一個socket對象,創(chuàng)建的時候同時指定服務(wù)器的IP和端口號,然后把它們傳入到socket中,這個過程就會讓客戶端和服務(wù)器建立連接,這就是三次握手,這個過程就是內(nèi)核完成的;
2)這時客戶端就可以通過Socket對象中的getInputStream和getOutputstream,來得到一個字節(jié)流對象,這時就可以與服務(wù)器進(jìn)行通信了;
3)在讀的時候,要注意此時只寫一個Socket文件,直接把服務(wù)器的端口號和IP地址,傳入進(jìn)去進(jìn)行構(gòu)造,就自動地和客戶端進(jìn)行了連接;此時還要有InputStreamSocket和OutstreamSocket;此時要把屏幕中讀到的字符串想辦法和Socket關(guān)聯(lián)起來,按我的理解來說,發(fā)送數(shù)據(jù)直接依靠println,讀取數(shù)據(jù)直接依靠scan.next即可 先啟動服務(wù)器,再啟動客戶端
——————————————————————————————————————
1)對于accept的返回值對應(yīng)Socket這個文件,是有一個close方法的,如果打開一個文件后,是要記得去關(guān)閉文件的,如果不關(guān)閉,就可能會造成文件資源泄漏的情況;一個socket文件寫完之后本應(yīng)是要關(guān)閉的,但是咱們前面寫的UDP程序是不可以關(guān)閉的,當(dāng)時的socket是有生命周期的,都是要跟隨整個程序的,當(dāng)客戶端不在工作時,進(jìn)程都沒了,PCB也沒了,文件描述符就更沒有了
2)咱們的TCP服務(wù)器有一個listenSocket,但是會有多個clientsocket,可以說每一個客戶端,每一個請求都對應(yīng)一個climentSocket;如果這個客戶端斷開鏈接了,對應(yīng)的clientSocket也就需要銷毀了
下面是客戶端的代碼:
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; import java.net.Socket; import java.util.Scanner;class Request{int serverport=0;String serverIp=null;Socket socket=null;public Request(String serverIp,int serverport)throws IOException {this.serverIp=serverIp;this.serverport=serverport;this.socket=new Socket(serverIp,serverport); //讓Socket創(chuàng)建的同時,就與服務(wù)器建立了鏈接,相當(dāng)于撥電話的操作,這個操作對標(biāo)于服務(wù)器中的climentSocket.accept操作,我們客戶端的IP地址就是本機(jī)的IP地址,咱們的端口號是由系統(tǒng)自動進(jìn)行分配的 //我們在這里面?zhèn)魅氲腎P和端口號不是自己進(jìn)行綁定,而是表示和這個IP端口建立連接}public void start()throws IOException { //1從鍵盤也就是控制臺上讀取請求Scanner scan = new Scanner(System.in); try (InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()) {while (true) {System.out.println("請輸入你的請求內(nèi)容");System.out.println("->");String request = scan.next();if (request.equals("goodbye")) {System.out.println("即將退出客戶端");break;}//2把這個從客戶端讀取的內(nèi)容,構(gòu)造請求發(fā)給服務(wù)器PrintStream printStream=new PrintStream(outputStream);printStream.println(request); //在這里我們懷疑println不是把數(shù)據(jù)發(fā)送到服務(wù)器中了,而是放到緩沖區(qū)里面了,我們刷新一下緩沖區(qū),強(qiáng)制進(jìn)行發(fā)送printStream.flush();//如果不刷新,服務(wù)器無法及時收到數(shù)據(jù)//3我們從服務(wù)器那邊讀取響應(yīng),并進(jìn)行解析Scanner scanreturn=new Scanner(inputStream);String response=scanreturn.next();//System.out.println(1); //4我們把結(jié)果顯示在控制臺上面String string=String.format("請求是 %s,回應(yīng)是 %s",request,response);System.out.println(string);}}catch(IOException e){e.printStackTrace();}}public static void main(String[] args) throws IOException {Request request=new Request("127.0.0.1",9090);request.start();} }下面是服務(wù)端的代碼:
我們一般是先啟動服務(wù)器,再啟動客戶端;
1)當(dāng)啟動服務(wù)器的時候,此時的服務(wù)器就會阻塞到accept這里,此時還沒客戶端建立連接;
2)客戶端在這里再啟動,就會調(diào)用Socket的構(gòu)造方法,在構(gòu)造方法中就會和服務(wù)器建立連接;
3)當(dāng)客戶端和服務(wù)器建立連接后,accept方法就會返回;
4)procession方法中,就會進(jìn)入循環(huán),嘗試讀取數(shù)據(jù),就會阻塞在next方法中,當(dāng)客戶端真正發(fā)請求為止;
5)此時的客戶端也進(jìn)入到start中的next方法中,也會阻塞在next方法中,等待用戶在鍵盤上輸入一個字符串;
6)當(dāng)下的狀態(tài)是客戶端阻塞在用戶往建盤中輸入數(shù)據(jù),服務(wù)器阻塞在等待客戶端的請求;接下來,用戶輸入數(shù)據(jù)的時候,此時客戶端的阻塞就結(jié)束了,然后回發(fā)送一個數(shù)據(jù)給服務(wù)器(Outputstream),同時服務(wù)器就會從等待讀取客戶端請求的狀態(tài)中恢復(fù)過來,執(zhí)行后面的procession邏輯;
7)當(dāng)客戶端發(fā)送完請求之后,又會在第二個next方法里面進(jìn)行阻塞,等待服務(wù)器的響應(yīng)的數(shù)據(jù);
8)服務(wù)器響應(yīng)并把數(shù)據(jù)發(fā)送回去后,客戶端才會在第二個next的等待中返回;
咱們的服務(wù)器要想拿到客戶端的端口號就要通過climentSocket(Socket創(chuàng)建的實例)
IP地址:climentSocket.getInetAddress().toString();
端口號:climentSocket.getPort();
9)我們要使用printWriter中的println方法而不是write();
package API;import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.Scanner;public class TCPServer {ServerSocket listenSocket=null;public TCPServer(int serverPort) throws IOException {listenSocket=new ServerSocket(serverPort);//在一開始服務(wù)器進(jìn)行啟動的時候,我們就需要指定一個端口,后續(xù)客戶端要根據(jù)這個端口來進(jìn)行訪問,服務(wù)器的IP地址默認(rèn)是主機(jī)IP//后續(xù)客戶端進(jìn)行訪問服務(wù)器的時候,目的IP就是服務(wù)器的IP,不需要我們服務(wù)器的開發(fā)者來進(jìn)行綁定了,只要我們確定服務(wù)器在一臺電腦上//IP地址就是確定了}public void start() throws IOException {System.out.println("TCP服務(wù)器開始進(jìn)行啟動");while(true){//1.由于咱們的TCP是有連接的,我們不能一上來就進(jìn)行讀數(shù)據(jù),而是我們要先進(jìn)行接電話操作(房屋中介)//2.咱們的accept操作,本質(zhì)上就是相當(dāng)于是接電話,接電話的前提是,必須是有人給你打了電話,如果說此時沒有客戶端和你建立連接//3.這個accept方法就會阻塞,直到有人向他建立了連接//4.因為我們的客戶端的請求的主機(jī)可能有多份,所以我們的每一個客戶端主機(jī)都會有一個climentSocket來進(jìn)行處理//UDP的服務(wù)器進(jìn)入主循環(huán)之后,就嘗試用receive讀取請求了,這是無連接的 //但是我們的TCP是有連接的,首先需要做的事,先建立起連接,相當(dāng)于客戶端的代碼是貨品,就要通過一條公路來把這些貨發(fā)送過去 //當(dāng)服務(wù)器運行的時候,是否有客戶端來建立連接,不確定,如果客戶端沒有建立連接,accept就會進(jìn)行阻塞等待 // 如果有連接,accept方法就會返回一個Socket對象,也就是說進(jìn)一步的客戶端和服務(wù)器的交互就交給Socket clientSocket= listenSocket.accept();procession(clientSocket);}}private void procession(Socket clientSocket) throws IOException {System.out.printf("我們這一次客戶端請求的IP地址是%s 端口號是%d",clientSocket.getInetAddress().toString(),clientSocket.getPort());//對于每一次請求,我們都要創(chuàng)建一個procession方法來進(jìn)行處理,我們接下來就來處理請求和響應(yīng)//這里面的針對TCP Socket的文件讀寫是和文件讀寫是一模一樣的//我們在里面主要是對socket文件來進(jìn)行讀和寫//TCP和UDP是全雙工,我們既可以讀Socket文件,也可以寫Socket文件try(InputStream inputStream=clientSocket.getInputStream()){try(OutputStream outputStream= clientSocket.getOutputStream()){ //我們來進(jìn)行循環(huán)處理請求,來進(jìn)行處理響應(yīng),我們的一臺主機(jī)是要給服務(wù)器發(fā)送多次請求的Scanner scanner=new Scanner(inputStream);while(true){if(!scanner.hasNext()){System.out.printf("客戶端斷開連接%s %d",clientSocket.getInetAddress(),clientSocket.getPort());break;}}//我們在這里面使用Scanner是更方便的,如果說我們不使用Scanner就需要進(jìn)行使用原生的inputStream中的read方法就可以了//只不過我們需要創(chuàng)建一個字節(jié)數(shù)組,然后使用stringbulider來進(jìn)行拼接// 1.讀取請求并進(jìn)行解析String request= scanner.next(); //2.根據(jù)請求計算并執(zhí)行邏輯,我們創(chuàng)建process方法執(zhí)行String response=process(request); //3.幫我們寫的邏輯返回給客戶端,為了方便起見,我們直接使用PrintWriter來進(jìn)行對OutputStream來進(jìn)行包裹一下PrintWriter printWriter=new PrintWriter(outputStream);printWriter.println(response);printWriter.flush();//我們在這里賣你要進(jìn)行刷新緩沖區(qū),如果沒有這個刷新,那么我們的客戶端時不能第一時間獲取到這個響應(yīng)結(jié)果 //4打印信息 System.out.printf("[客戶端的端口號是%d 客戶端的IP地址是%s],請求數(shù)據(jù)是%s,響應(yīng)數(shù)據(jù)是%s",clientSocket.getPort(),clientSocket.getInetAddress(),request,response);}} catch (IOException e) {e.printStackTrace();}finally {clientSocket.close();listenSocket.close();}}private String process(String request) {return request+"我愛你";}public static void main(String[] args) throws IOException {TCPServer server=new TCPServer(9099);server.start();} }1)在我們寫TCP服務(wù)器的時候,我們都針對了這里面的climentSocket(Socket創(chuàng)建的實例)關(guān)閉了一下,但是我們對于listenSocket(ServerSocket創(chuàng)建的實例)卻沒有進(jìn)行關(guān)閉
2)同時在UDP的代碼里面我們也沒有針對DatagramSocket對象和DatagramPacket來進(jìn)行關(guān)閉
catch (IOException e) {e.printStackTrace();}finally {clientSocket.close();//listenSocket.close();在這里面是不能進(jìn)行關(guān)閉的}這是為什么呢?
1)我們關(guān)閉的目的是為了釋放資源,釋放資源的一個前提是這個資源已經(jīng)不再進(jìn)行使用了,對于咱們的UDP程序和ServerSocket來說,這些Socket都是是重要貫穿始終的,只要程序啟動運行我們就要用到;
2)這些實例什么時候不用?啥時候咱們的服務(wù)器進(jìn)行關(guān)閉,啥時候不用;
3)咱們的這些資源最遲最遲也就會隨著進(jìn)程一起進(jìn)行釋放了,進(jìn)程才是操作系統(tǒng)分配資源的自小單位
4)但是咱們的climentSocket的生命周期是很短的,針對咱們的每一個客戶端程序,都要進(jìn)行創(chuàng)建一個climentSocket對象,當(dāng)我們的對應(yīng)的客戶端斷開連接之后,咱們的服務(wù)器的對應(yīng)的climentSocket對象也就永遠(yuǎn)不會再進(jìn)行試用了,我們就需要關(guān)閉文件釋放資源(咱們的climentSocket對象有很多)
——————————————————————————————————————————
利用UDP來創(chuàng)建一個字典服務(wù)器:
import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketException; import java.util.HashMap;public class Server {int port;DatagramSocket socket=null;HashMap<String,String> map=new HashMap<>();public Server(int port)throws SocketException {this.port = port;this.socket =new DatagramSocket(port);map.put("及時雨","宋江");map.put("國民女神","高圓圓");map.put("大聰明","李嘉欣");map.put("王者大神","李佳偉");}public void start()throws IOException{System.out.println("服務(wù)器即將啟動");while(true){DatagramPacket requestpacket=new DatagramPacket(new byte[4096],4096);socket.receive(requestpacket);System.out.println(1);String request=new String(requestpacket.getData(),0,requestpacket.getLength());String response=processon(request);DatagramPacket responsepacket=new DatagramPacket(response.getBytes(),response.getBytes().length,requestpacket.getSocketAddress());socket.send(responsepacket);String str=String.format("[%s:%d] request=%s;reponse=%s",requestpacket.getAddress(),requestpacket.getPort(),request,response);System.out.println(str);}}public String processon(String request){return map.getOrDefault(request,"你查詢的詞不存在");// return request;}public static void main(String[] args)throws SocketException,IOException{Server server=new Server(9090);server.start();}} import java.io.IOException; import java.net.*; import java.util.Scanner;public class user{DatagramSocket socket=null;int serverport;String serverIP;public user(int serverport,String serverIP)throws SocketException {this.serverIP=serverIP;this.serverport=serverport;this.socket=new DatagramSocket();}public void start()throws UnknownHostException, IOException{System.out.println("客戶端即將啟動");while(true){System.out.println("請輸入你的請求");Scanner scanner = new Scanner(System.in);System.out.println("->");String request=scanner.nextLine();if(request.equals("exit")){System.out.println("goodbye");return;}DatagramPacket requestpacket=new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(serverIP),serverport);socket.send(requestpacket);DatagramPacket responsepacket=new DatagramPacket(new byte[4096],4096);socket.receive(responsepacket);String response=new String(responsepacket.getData(),0,responsepacket.getLength());String str=String.format("request=%s,response=%s",request,response);System.out.println(str);}}public static void main(String[] args)throws SocketException,IOException{user user1=new user(9090,"127.0.0.1");user1.start();}}下面我們來測試一個程序TCP服務(wù)器
1)一個服務(wù)器對應(yīng)多個客戶端,此時就需要多次啟動這個客戶端實例;
現(xiàn)象是當(dāng)我們啟動第一個客戶端之后,服務(wù)器進(jìn)行提示上線,當(dāng)我們啟動第二個客戶端的時候,服務(wù)器此時就沒有任何響應(yīng)了,況且發(fā)送請求的時候沒有進(jìn)行任何響應(yīng);但是當(dāng)我們退出客戶端的時候,此時神奇的事情出現(xiàn)了,服務(wù)器提示了客戶端二上線,況且客戶端二也得到了服務(wù)器的響應(yīng),但是此時客戶端三沒有任何反應(yīng),當(dāng)我們把客戶端2的主機(jī)退出,那么客戶端3給服務(wù)器發(fā)送的數(shù)據(jù)就有效了
2)所以當(dāng)前的服務(wù)器,在同一時刻,只可以給一個客戶端提供服務(wù),只有前一個客戶端下了,下一個客戶端才可以上來;
3)當(dāng)咱們的第一個客戶端嘗試與服務(wù)器建立連接的時候,服務(wù)器會與客戶端建立連接,這個時候客戶端發(fā)送數(shù)據(jù),服務(wù)器就會做出響應(yīng),客戶端多次發(fā)送數(shù)據(jù),咱們的服務(wù)器就會循環(huán)處理請求;
3.1)調(diào)用listenSocket的accept方法,與客戶端建立連接
3.2)執(zhí)行process方法,來循環(huán)進(jìn)行處理客戶端給服務(wù)器發(fā)送過來的請求
4)但是此時我們的第二個,第三個,第四個客戶端想要給服務(wù)器發(fā)送數(shù)據(jù),不可能成功建立連接
5)原因是在服務(wù)器中的hasNext那里在等待第一個客戶端發(fā)送數(shù)據(jù),他并沒有退出第一個客戶端對應(yīng)的這個procession這個方法,也就是說直接死在第一個客戶端的procession這個方法的while循環(huán)里面(一直進(jìn)行工作),所以整個服務(wù)器的程序就卡死在hasNext這個代碼塊里面了,主函數(shù)的外層的while循環(huán)無法進(jìn)行下一輪,也就無法調(diào)用第二次accept方法,(服務(wù)器無法再次調(diào)用accept方法與下一個客戶端進(jìn)行三次握手,建立連接);
最終造成的結(jié)果是,客戶端什么時候退出,整個循環(huán)就啥時候結(jié)束;
4)所以問題的關(guān)鍵在于,如果第一個客戶端沒有退出,此時服務(wù)器的邏輯就一直在procession里面打轉(zhuǎn),也就沒有機(jī)會再次調(diào)用accept方法,也就無法再次去處理第二個連接;第一個客戶端退出以后,結(jié)束里面的循環(huán),結(jié)束上一個procession服務(wù)器才可以執(zhí)行到第二個accept,才可以建立連接;
5)我們這個問題就類似于,好像你接了一個電話,和對方你一言,我一語的進(jìn)行通話,別人再繼續(xù)給我們進(jìn)行打電話,我們就沒有辦法進(jìn)行接通了
6)咱們解決上述問題,就需要第一次執(zhí)行的procession方法,不能影響到咱們的下一次循環(huán)掃描accept的執(zhí)行;
public class Server {HashMap<String,String> map=new HashMap<>();int serverport=0;ServerSocket listenSocket=null;public Server(int serverport) throws IOException {this.serverport=serverport;this.listenSocket=new ServerSocket(serverport);map.put("及時雨","宋江");map.put("國民女神","高圓圓");map.put("老子","李佳偉");}public void start() throws IOException {System.out.println("服務(wù)器即將啟動");while(true){Socket climentSocket=listenSocket.accept(); //我們的改進(jìn)方案是每一次accept方法成功,那么我們就創(chuàng)建一個新的線程,有一個新的線程來執(zhí)行這次process方法,這樣就實現(xiàn)了代碼之間的解耦合Thread thread=new Thread(){public void run(){String str=String.format("客戶端的IP地址是 %s 客戶端對應(yīng)的端口號是 %d",climentSocket.getInetAddress().toString(),climentSocket.getPort());System.out.println(str);try {procession(climentSocket);} catch (IOException e) {e.printStackTrace();}}};thread.start();}}public void procession(Socket climentSocket) throws IOException {try(InputStream inputStream=climentSocket.getInputStream();OutputStream outputStream=climentSocket.getOutputStream()){Scanner scanner=new Scanner(inputStream);PrintStream printStream=new PrintStream(outputStream);while(true){if(!scanner.hasNext()){System.out.println("這個客戶端對應(yīng)的服務(wù)器已經(jīng)完成了工作");return;}String request=scanner.next();String response=hhhh(request);printStream.println(response);String str=String.format("請求是 %s 響應(yīng)是 %s",request,response);System.out.println(str);}}}public String hhhh(String request){return map.getOrDefault(request,"沒有這個參數(shù)");} 這是改進(jìn)后的服務(wù)器代碼引入多線程之后,保證主線程始終在調(diào)用accept,每次都有一個新的連接來創(chuàng)建新線程來處理請求響應(yīng),線程都是一個獨立的執(zhí)行流,每一個線程都會執(zhí)行自己的同一段邏輯,并發(fā)執(zhí)行
1)咱們剛才的UDP版本的程序就沒有用到多線程?因為咱們的UDP編程不需要處理連接,咱們的UDP只需要一個循環(huán),就可以處理所有客戶端的請求
2)DatagramPacket requestpacket=new DatagramPacket(new Byte[4096],4096);
requestSocket.receive(requestPacket);這個start方法進(jìn)入循環(huán),不管誰,不管哪一個客戶端發(fā)送過來了請求,服務(wù)器都會進(jìn)行處理返回,一個循環(huán)把所有客戶端都給伺候好了
3)但是咱們的TCP機(jī)既要處理連接,又要處理一個連接中的若干次請求,就需要兩個循環(huán),里層循環(huán)就會影響外層循環(huán)的進(jìn)度了
4)主線程循環(huán)調(diào)用accept方法,當(dāng)我們有客戶端嘗試進(jìn)行連接的時候,我們直接讓主線程創(chuàng)建一個新線程,由新線程負(fù)責(zé)并發(fā)處理若干個客戶端的請求,在新線程里面,我們通過while循環(huán)來進(jìn)行處理請求,這個時候,多線程就是并發(fā)執(zhí)行的關(guān)系了,就是各自執(zhí)行各自的,彼此之間不會相互干擾
2)但是在實際開發(fā)中,客戶端的數(shù)目可能有很多,這個時候可以嗎?
雖然線程比進(jìn)程更輕量,但是如果有很多的客戶端連接又退出,這就會導(dǎo)致咱們當(dāng)前服務(wù)器頻繁的創(chuàng)建銷毀線程,如何改進(jìn)這個問題?
這時我們就需要用到線程池了
3)當(dāng)我們的客戶端new Socket()成功的時候,其實本質(zhì)上從操作系統(tǒng)內(nèi)核層面,已經(jīng)建立好了連接(TCP三次握手),但是咱們的應(yīng)用程序沒有建立這個鏈接
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; import java.net.ServerSocket; import java.net.Socket; import java.util.HashMap; import java.util.Scanner; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;public class Server {HashMap<String,String> map=new HashMap<>();int serverport=0;ServerSocket listenSocket=null;public Server(int serverport) throws IOException {this.serverport=serverport;this.listenSocket=new ServerSocket(serverport);map.put("及時雨","宋江");map.put("國民女神","高圓圓");map.put("老子","李佳偉");}public void start() throws IOException {System.out.println("服務(wù)器即將啟動");while(true){Socket climentSocket=listenSocket.accept();String str=String.format("客戶端的IP地址是 %s 客戶端對應(yīng)的端口號是 %d",climentSocket.getInetAddress().toString(),climentSocket.getPort());System.out.println(str);ExecutorService executorService= Executors.newCachedThreadPool();executorService.submit(new Runnable() {@Overridepublic void run() {try {procession(climentSocket);} catch (IOException e) {e.printStackTrace();}}});}}public void procession(Socket climentSocket) throws IOException {try(InputStream inputStream=climentSocket.getInputStream();OutputStream outputStream=climentSocket.getOutputStream()){Scanner scanner=new Scanner(inputStream);PrintStream printStream=new PrintStream(outputStream);while(true){if(!scanner.hasNext()){System.out.println("這個客戶端對應(yīng)的服務(wù)器已經(jīng)完成了工作");return;}String request=scanner.next();String response=hhhh(request);printStream.println(response);String str=String.format("請求是 %s 響應(yīng)是 %s",request,response);System.out.println(str);}}}public String hhhh(String request){return map.getOrDefault(request,"沒有這個參數(shù)");}public static void main(String[] args) throws IOException {Server server=new Server(8080);server.start();} }假設(shè)極端情況下,一個服務(wù)器面臨著很多很多客戶端,這些客戶端,連接上了并沒有退出,這個時候服務(wù)器這邊,就會存在很多很多線程,會有上萬個線程?這個情況下,會有一些其他的問題嗎?這是科學(xué)的解決方法嗎?
1)實際上這種現(xiàn)象是不安全的,不科學(xué),每一個線程都會占據(jù)一定的系統(tǒng)資源,如果線程太多太多了,這時候許多系統(tǒng)資源(內(nèi)存資源+CPU資源)就會十分緊張,達(dá)到一定程度,機(jī)器可能就會宕機(jī)(因為你創(chuàng)建線程就要有PCB,還要為這個線程分配棧和程序計數(shù)器的空間)
上萬個線程會搶這一兩個CPU,操作系統(tǒng)的內(nèi)核就會十分激烈,線程之間的調(diào)度開銷是十分激烈的,比如說線程1正在執(zhí)行,執(zhí)行一會被操作系統(tǒng)的內(nèi)核調(diào)度出去了,下一次啥時候上CPU執(zhí)行,就不知道了,因為排的隊,實在是太多太多了,線程之間非常卷,服務(wù)器對于客戶端的響應(yīng)能力,返回數(shù)據(jù)的時間,就會大大降低了;
2)例如在雙十一/春運這種場景,如果一個系統(tǒng),同時收到太高的并發(fā),就可能會出現(xiàn)問題,例如每一個并發(fā)都需要消耗系統(tǒng)資源,并發(fā)多了,系統(tǒng)資源消耗就多了,系統(tǒng)剩下的資源就少了,響應(yīng)能力就變慢了,再進(jìn)一步,把系統(tǒng)都給消耗沒了,系統(tǒng)也就無法正常工作了(我們的服務(wù)器不可能響應(yīng)無數(shù)個客戶端)
1)使用攜程來進(jìn)行代替線程,完成并發(fā),很多協(xié)程的實現(xiàn),是一個M:N的關(guān)系(一大堆的攜程是通過一個線程來進(jìn)行完成的),協(xié)程比線程還要輕量
2)使用IO多路復(fù)用的機(jī)制,完成并發(fā);
2.1)這會從根本上來解決服務(wù)器高并發(fā)的這樣一個問題,在內(nèi)核里面來支持這樣的功能
2.2)假設(shè)現(xiàn)在有1W個客戶端,在這個服務(wù)器里面就會用一定的數(shù)據(jù)結(jié)構(gòu)把1W個客戶端對應(yīng)的Socket保存好,不需要一個線程對應(yīng)一個客戶端,一共就有幾個線程,IO多路賦用機(jī)制,就可以做到,哪個Socket上面有數(shù)據(jù)了,就通知到這個應(yīng)用程序,讓這個線程從這個socket里面來讀數(shù)據(jù);雖然是1w個客戶端,但是在同一時刻,也就只有不到1K個客戶端來給服務(wù)器發(fā)送請求;靠系統(tǒng)來通知應(yīng)用程序,誰可以讀,就去讀誰,通過一個線程就可以處理多個Socket,在C++方向,典型實現(xiàn)就是epoll(mac/linux),kqueue(windows),我們在Java里面通過NIO這樣的一系列的庫,封裝了epoll等IO多路復(fù)用機(jī)制
3)使用多個服務(wù)器(分布式),就算我們使用協(xié)程,就算使用IO多路復(fù)用,咱們的系統(tǒng)同一時刻處理的線程數(shù)仍然是有限的,只是節(jié)省了一個客戶端對應(yīng)的資源,但是隨著客戶端的增加,還是會消耗更多的資源,我們就使用更多的硬件資源,此時每一臺主機(jī)承受的壓力就小了
以上三種都是一個基本處理高并發(fā)場景的,所使用的方法
咱們的一個TCP服務(wù)器,是否可以讓一個UDP客戶端連接上呢?
1)TCP和UDP,他們無論是API代碼,還是協(xié)議底層的工作過程,都是差異巨大的,不是單純的把流轉(zhuǎn)化成數(shù)據(jù)包就可以的
2)一次通信,我們使用5元組,協(xié)議類型不匹配,通信時無法進(jìn)行完成的
總結(jié)
以上是生活随笔為你收集整理的网络编程(3)-----------Javaweb的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【uniapp 动态设置 起始页 默认展
- 下一篇: Java实现MD5加密解密