目錄
4、TCP網絡傳輸?shù)幕玖鞒?/p>
二、網絡編程套接字(socket)
5、cookie和session的用法
6、基本實現(xiàn)http協(xié)議的代碼
四、傳輸層協(xié)議TCP和UDP
4、TCP和UDP之間的對比
六、數(shù)據(jù)鏈路層和應用層(以太網協(xié)議和DNS協(xié)議)
一、網絡通信協(xié)議
1、概述
計算機網絡是通過 傳輸介質、通信設施和網絡通信協(xié)議 ,把分散在不同地點的計算機設備互連起來的,實現(xiàn)資源共享和數(shù)據(jù)傳輸?shù)南到y(tǒng)。網絡編程就是編寫程序使互聯(lián)網的兩個(或多個)設備(如計算機)之間進行數(shù)據(jù)傳輸。JAVA語言對網絡編程提供了良好的支持。通過其提供的接口我們可以很方便地進行網絡編程。
網絡通信協(xié)議是網絡編程中的關鍵,通信雙方達成的一種共識,當通信雙方都遵守這樣的約定,才能正確傳輸信息。
2、為什么協(xié)議要分層?
1、分層可以避免一個協(xié)議太過于龐大和復雜。
2、分層之后協(xié)議之間 解耦合 ,上層協(xié)議不需要理解下層協(xié)議的細節(jié)
3、任意層次的協(xié)議的可以進行靈活的替換
3、對于參考模型的介紹
在這里我們對于TCP/IP模型進行詳細的描述:
應用層:和應用程序直接打交道的協(xié)議。
傳輸層:負責端到端之間的傳輸(關注起點和終點)
網絡層:負責點到點之間的傳輸(關注傳輸?shù)穆窂剑?/h1>
數(shù)據(jù)鏈路層:負責相鄰點之間如何具體傳輸。
物理層:網絡通信的基礎硬件措施。
面試常見:
1、對于一臺主機來說,它的操作系統(tǒng)內核實現(xiàn)了從應用層到物理層
2、對于一臺路由器來說,它實現(xiàn)了從網絡層到物理層。
3、對于一臺交換機來說,它實現(xiàn)了從數(shù)據(jù)鏈路層到物理層。
4、對于集線器來說,它實現(xiàn)的物理層。
4、TCP網絡傳輸?shù)幕玖鞒?/h1>
在這些基本操作中涉及到 封裝 和 分用 兩個操作流程。
封裝的意思就是給基于數(shù)據(jù)的基礎上,在數(shù)據(jù)前加上協(xié)議報頭,當數(shù)據(jù)由應用層傳輸?shù)絺鬏攲訒r,傳輸層會在得到的數(shù)據(jù)基礎上加上傳輸層協(xié)議報頭。 當數(shù)據(jù)由傳輸層到達網絡層時,會繼續(xù)添加網絡層協(xié)議報頭,也就是我們常說的IP地址。當數(shù)據(jù)由網絡層傳輸?shù)綌?shù)據(jù)鏈路層時會在數(shù)據(jù)前加上數(shù)據(jù)鏈路層協(xié)議報頭,也就是常數(shù)的mac地址。數(shù)據(jù)鏈路層將數(shù)據(jù)發(fā)送給物理層時,物理層就會將這個數(shù)據(jù)轉化成光電信號,通過一些硬件設備,例如網線,光纖,電磁波等傳輸出去。
分用的過程正好與封裝相反,當物理層接收到對方發(fā)送過來的光電信號時,會將它解析成二進制的bit流,進一步得到數(shù)據(jù)鏈路層數(shù)據(jù)幀。 數(shù)據(jù)鏈路層解析數(shù)據(jù)幀,玻璃針頭和針尾取出,其中的IP數(shù)據(jù)報交給網絡層。網絡層接收到剛才的網絡層數(shù)據(jù)報,通過解析去掉網絡層的協(xié)議報頭。把數(shù)據(jù)交給傳輸層,傳輸層拿到傳輸層,數(shù)據(jù)報在進行解析,去掉傳輸層報頭,最后將數(shù)據(jù)交給應用層,這時應用層就可以解析數(shù)據(jù),分析出發(fā)送者是誰,接收者是誰顯示到桌面上。
當發(fā)送者通過自己的主機將數(shù)據(jù)封裝完成之后,會通過路由器廣域網等發(fā)送到服務器。當服務器通過分用把數(shù)據(jù)拆開之后看到接收方的地址,然后服務器將數(shù)據(jù)重新封裝之后,通過路由器廣域網等途徑發(fā)送到接收者的主機,然后接收者的主機一步一步的進行分傭,最終得到數(shù)據(jù)并且顯示出來。
二、網絡編程套接字(socket)
套接字(socket)是一組API,用來實現(xiàn)網絡編程,一般是通過客戶端(server)和服務器(cilent)實現(xiàn)的。
IP地址:用來識別互聯(lián)網上一臺主機的位置。
端口號:用來區(qū)分是主機上的那個應用。
在一次通信過程中涉及到五元組: 源IP , 目的IP , 源端口 , 目的端口 , 協(xié)議類型 。
JAVA中提供了兩種風格:
UDP(DatagramSocket面向數(shù)據(jù)報,發(fā)送和接受數(shù)據(jù)要以數(shù)據(jù)包為單位)
TCP(ServerSocket面向字節(jié)流)
1、UDP(面向數(shù)據(jù)報)
1)UDP的服務器
//UDP的服務器
import java.io.IOException;
import java.NET.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class Test01 {
//進行實例化操作
private DatagramSocket socket=null;
//端口號
public Test01(int port) throws SocketException {
socket=new DatagramSocket(port);
}
//開啟服務器
public void start() throws IOException {
System.out.println("開啟服務器");
while (true){
//1、讀取請求并分析
DatagramPacket requstPacket=new DatagramPacket(new byte[4096],4096);
socket.receive(requstPacket);
String reques=new String(requstPacket.getData(),0,
requstPacket.getLength()).trim();
//2、請求數(shù)據(jù)相應
String response=process(reques);
//3、把響應返回給服務器
DatagramPacket responsePacket=new DatagramPacket(response.getBytes()
,response.getBytes().length,requstPacket.getSocketAddress());
socket.send(responsePacket);
//日志
System.out.printf("[%s:%d] req: %s; resp:%s n",requstPacket.getAddress().toString(),
requstPacket.getPort(),reques,response);
}
}
private String process(String reques) {
//我們寫最簡單的服務器回溯
return reques;
}
public static void main(String[] args) throws IOException {
Test01 test01=new Test01(9090);
test01.start();
}
}
2)UDP的客戶端
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class Test02 {
// 客戶端的主要流程分成四步.
// 1. 從用戶這里讀取輸入的數(shù)據(jù).
// 2. 構造請求發(fā)送給服務器
// 3. 從服務器讀取響應
// 4. 把響應寫回給客戶端.
private DatagramSocket socket = null;
private String serverIp;
private int serverPort;
// 需要在啟動客戶端的時候來指定需要連接哪個服務器
public Test02(String serverIp, int serverPort) throws SocketException {
this.serverIp = serverIp;
this.serverPort = serverPort;
socket = new DatagramSocket();
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while (true) {
// 1. 讀取用戶輸入的數(shù)據(jù)
System.out.print("-> ");
String request = scanner.nextLine();
if (request.equals("exit")) {
break;
}
// 2. 構造請求發(fā)送給服務器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
request.getBytes().length, InetAddress.getByName(serverIp), serverPort);
socket.send(requestPacket);
// 3. 從服務器讀取響應
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(), 0, responsePacket.getLength()).trim();
// 4. 顯示響應數(shù)據(jù)
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
Test02 client = new Test02("127.0.0.1", 9090);
// UdpEchoClient client = new UdpEchoClient("47.98.116.42", 9090);
client.start();
}
}
2、TCP(面向字節(jié)流)
客戶端發(fā)送的數(shù)據(jù)是按行讀取(n)
1)服務器
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test05 {
// 1. 初始化服務器
// 2. 進入主循環(huán)
// 1) 先去從內核中獲取到一個 TCP 的連接
// 2) 處理這個 TCP 的連接
// a) 讀取請求并解析
// b) 根據(jù)請求計算響應
// c) 把響應寫回給客戶端
private ServerSocket serverSocket=null;
public Test05(int port) throws IOException {
//綁定端口號
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服務器啟動");
//創(chuàng)建一個線程池
ExecutorService executorService= Executors.newCachedThreadPool();
while(true){
//先從內核中獲取到TCP連接
Socket clientSocket=serverSocket.accept();
//處理這個連接
//我們可以在這個地方加上一個線程池
executorService.execute(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
private void processConnection(Socket clientSocket) {
//獲取地址和端口
System.out.printf("[%s:%d] 客戶端上線 n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
try(BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))){
//長連接版本服務器
while(true){
//讀取
String request=bufferedReader.readLine();
//處理數(shù)據(jù)
String response=process(request);
//將這個返回給客戶端
bufferedWriter.write(response+"n");
bufferedWriter.flush();
//寫一個日志
System.out.printf("[%s:%d] req:%s,resq:%sn",clientSocket.getInetAddress().toString()
,clientSocket.getPort(),request,response);
}
} catch (IOException e) {
//e.printStackTrace();
System.out.printf("[%s:%d] 客戶端下線n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
Test05 test05=new Test05(9090);
test05.start();
}
}
2)客戶端
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class Test04 {
// 1. 啟動客戶端(一定不要綁定端口號) 和服務器建立連接
// 2. 進入主循環(huán)
// a) 讀取用戶輸入內容
// b) 構造一個請求發(fā)送給服務器
// c) 讀取服務器的響應數(shù)據(jù)
// d) 把響應數(shù)據(jù)顯示到界面上.
private Socket socket=null;
public Test04(String socketIP,int socketPort) throws IOException {
socket=new Socket(socketIP,socketPort);
}
public void start(){
System.out.println("啟動客戶端");
Scanner scanner=new Scanner(System.in);
try(BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) {
while(true){
System.out.print("-> ");
String request=scanner.nextLine();
if(request.equals("exit")){
break;
}
//發(fā)送給服務器
bufferedWriter.write(request+"n");
//刷新 因為寫入到緩沖區(qū)中并沒有寫入到內核中
bufferedWriter.flush();
//讀取服務器中的內容
String response=bufferedReader.readLine();
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
Test04 test04=new Test04("127.0.0.1",9090);
test04.start();
}
}
注意:
bufferedWriter中的write方法可能寫到緩沖區(qū)中沒有寫入內核中,需要使用flush方法來進行刷新。
如果要多個客戶端可以同時相應我們可以通過一個線程池來控制多個客戶端。
三、應用層協(xié)議HPPT
hppt和hppts都是應用層協(xié)議,應用層協(xié)議大多都需要手動指定,hppt協(xié)議是基于TCP來實現(xiàn)的。
1、URL
當你打開一個網頁的時候,會出現(xiàn)一個網址這個網址就是URL。
url中的服務器的ip來確定是哪一個服務器,
url中的端口號來確定是哪個進程。
url中的path來確定是哪個進程所管理的具體文件。
https://www.baidu.com/s? cl=3 &tn=baidutop10 &fr=top1000 &wd=%E6%95%B0%E8%AF%BB%E5%8D%81%E4%B9%9D%E5%B1%8A%E5%85%AD%E4%B8%AD%E5%85%A8%E4%BC%9A%E7%B2%BE%E7%A5%9E &rsv_idx=2 &rsv_dl=fyb_n_homepage &sa=fyb_n_homepage &hisfilter=1
這種鍵值對是特殊約定的事物。
2、抓包(Fiddler)
什么是抓包?我用一張圖來描述
fiddler不會對傳輸數(shù)據(jù)進行加工和修改,只會講數(shù)據(jù)截取下來讓用戶看到。
HPPT請求:
1)首行: 方法(GET) URL 版本號
2)協(xié)議頭(header):若干個鍵值對用(:)來分割
3)空行:header的結束標記
4)正文(body):對于get來說為空,對于post來說不為空
HPPT相應:
1)首行: 版本號 狀態(tài)碼 狀態(tài)碼對應的描述信息
2)協(xié)議頭(header):若干個鍵值對用(:)來分割
3)空行:header的結束標記
4)正文(body):常見的格式為html格式,部分可能存在加密的情況
3、HPPT的方法
GET和POST的區(qū)別?
GET一般把數(shù)據(jù)放到url中去
Post一般把數(shù)據(jù)放到body中去
4、狀態(tài)碼的詳解
5、cookie和session的用法
cookie可以將登錄認證后的用戶信息保存在本地瀏覽器上,當后面每次發(fā)送http請求時,都會附帶上這個信息,就不需要每次都重新驗證用戶。
但是這樣明文傳輸cookie可能會泄密在這個前提下我們引入session來保存數(shù)據(jù)
在服務器登錄成功時,我們把用戶保存到一個hash表中,同時生成一個key(sessionid),把sessionid寫回到這個cookie中去,當后續(xù)訪問時,只需要在通過sessionid訪問服務器中就行了。
6、基本實現(xiàn)http協(xié)議的代碼
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
public class HttpRequset {
private String method;//方法
private String url;//url
private String version;//版本號
//協(xié)議頭
private Map<String,String> headers=new HashMap<>();
//url中和body中的內容放在這里
private Map<String,String> parameters=new HashMap<>();
//cookie中的類容解析
private Map<String,String> cookies=new HashMap<>();
private String body;//body
//用工廠模式來寫
public static HttpRequset build(InputStream inputStream) throws IOException {
HttpRequset requset=new HttpRequset();
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(inputStream));
//1、解析首行
String firstline=bufferedReader.readLine();
String[] fiestlines=firstline.split(" "); //第一行是通過空格來區(qū)分的
requset.method=fiestlines[0];
requset.url=fiestlines[1];
requset.version=fiestlines[2];
//2、將首行中的url中的元素提取出來
int pre=requset.url.indexOf("?");//如果沒有就返回-1
if(pre!=-1){
String qureyString=requset.url.substring(pre+1);
parseKV(qureyString,requset.parameters);
}
//3、將headers中的內容解析出來
String line="";
while((line=bufferedReader.readLine())!=null && line.getBytes().length!=0){
String[] lines=line.split(": ");
requset.headers.put(lines[0],lines[1]);
}
//4、解析cookie中的內容
String cookie=requset.headers.get("Cookie");
if(cookie!=null){
parseCookie(cookie,requset.cookies);
}
//5、body中的內容
if("POST".equalsIgnoreCase(requset.method) || "PUT".equalsIgnoreCase(requset.method)){
//我們首先要知道body的長度
int length=Integer.parseInt(requset.headers.get("Content-Length"));
//建立一個緩沖區(qū)
char[] buffer=new char[length];
int len=bufferedReader.read(buffer);
requset.body=new String(buffer,0,len);
parseKV(requset.body,requset.parameters);
}
return requset;
}
private static void parseCookie(String cookie, Map<String, String> cookies) {
String[] line=cookie.split("; ");
for(String s:line){
String[] lines=s.split("=");
cookies.put(lines[0],lines[1]);
}
}
private static void parseKV(String qureyString, Map<String, String> parameters) {
String[] line=qureyString.split("&");
for (String s:line){
String[] lines=s.split("=");
parameters.put(lines[0],lines[1]);
}
}
public String getMethod() {
return method;
}
public String getUrl() {
return url;
}
public String getVersion() {
return version;
}
public String getBody() {
return body;
}
public String getHeaders(String key){
return headers.get(key);
}
public String getParameters(String key){
return parameters.get(key);
}
public String getCooies(String key){
return cookies.get(key);
}
}
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.HashMap;
import java.util.Map;
public class HttpResponse {
private String version="HTTP/1.1";//版本號
private int status;//驗證信息
private String message;//狀態(tài)信息
//header中的內容
private Map<String,String> headers=new HashMap<>();
//body中的內容
private StringBuffer body=new StringBuffer();
private OutputStream outputStream=null;
//工廠模式
public static HttpResponse build(OutputStream outputStream){
HttpResponse response=new HttpResponse();
response.outputStream=outputStream;
return response;
}
public void setVersion(String version) {
this.version = version;
}
public void setStatus(int status) {
this.status = status;
}
public void setMessage(String message) {
this.message = message;
}
public void setHeaders(String key,String value){
this.headers.put(key,value);
}
public void setBody(String val){
this.body.Append(val);
}
public void fush() throws IOException {
BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(outputStream));
//寫第一行
bufferedWriter.write(version+" "+status+" "+message+"n");
//將文件信息放入header中寫入
headers.put("Content-Length",body.toString().getBytes().length+"");
//將header中的類容寫入
for(Map.Entry<String,String> entry:headers.entrySet()){
bufferedWriter.write(entry.getKey()+": "+entry.getValue()+"n");
}
//
bufferedWriter.write("n");
//將body中的數(shù)據(jù)放入
bufferedWriter.write(body.toString());
//將數(shù)據(jù)刷新
bufferedWriter.flush();
}
}
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<form method="post" action="/login">
<div style="margin-bottom: 5px">
<input type="text" name="username" placeholder="請輸入姓名">
</div>
<div style="margin-bottom: 5px">
<input type="text" name="password" placeholder="請輸入密碼">
</div>
<div>
<input type="submit" value="登錄">
</div>
</form>
</body>
</html>
import Test05.HttpServerV3;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class HttpServerV4 {
static class User{
//保存的相關信息
public String userName;
public int age;
public String school;
}
private ServerSocket socket=null;
//我們通過session會話
private Map<String,User> session=new HashMap<>();
public HttpServerV4(int port) throws IOException {
this.socket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("系統(tǒng)啟動");
ExecutorService executorService= Executors.newCachedThreadPool();
while(true){
Socket cliensocket=socket.accept();
executorService.execute(new Runnable() {
@Override
public void run() {
process(cliensocket);
}
});
}
}
private void process(Socket cliensocket) {
try {
//讀取請求
HttpResponse response=HttpResponse.build(cliensocket.getOutputStream());
HttpRequset requset=HttpRequset.build(cliensocket.getInputStream());
//response.setHeaders("Set-Cookie","YYX=qwe");
//計算相應
if("GET".equalsIgnoreCase(requset.getMethod())){
doGet(requset,response);
}else if("Post".equalsIgnoreCase(requset.getMethod())){
doPost(requset,response);
}else{
response.setStatus(405);
response.setMessage("Method Not Allowed");
}
//寫入
response.fush();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
cliensocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void doPost(HttpRequset requset, HttpResponse response) {
//2
if(requset.getUrl().startsWith("/login")){
String user=requset.getParameters("username");
String pass=requset.getParameters("password");
/*System.out.println(user+":"+pass);*/
if("yyx".equals(user) && "2001".equals(pass)){
//通過
response.setStatus(403);
response.setMessage("Forbidden");
//要寫不然返回的不是utf-8的編碼格式
response.setHeaders("Content-Type","text/html; charset=utf-8");
//response.setHeaders("Set-Cookie","userName=YYX");
String sessionId= UUID.randomUUID().toString();
User user01=new User();
user01.userName="楊亞軒";
user01.age=18;
user01.school="武昌工學院";
session.put(sessionId,user01);
response.setHeaders("Set-Cookie","sessionId="+sessionId);
response.setBody("<html>");
response.setBody("驗證成功!");
response.setBody("</html>");
}else {
//沒有通過
response.setStatus(403);
response.setMessage("Forbidden");
//要寫不然返回的不是utf-8的編碼格式
response.setHeaders("Content-Type","text/html; charset=utf-8");
response.setBody("<html>");
response.setBody("驗證失敗!");
response.setBody("</html>");
}
}
}
private void doGet(HttpRequset requset, HttpResponse response) throws IOException {
//1
if(requset.getUrl().startsWith("/index.html")){
String sessionId=requset.getCooies("sessionId");
User user=session.get(sessionId);
if (sessionId==null || user==null) {
//當前情況下用戶沒有登錄
response.setStatus(200);
response.setMessage("OK");
response.setHeaders("Content-Type","text/html; charset=utf-8");
InputStream inputStream=HttpServerV3.class.getClassLoader().getResourceAsStream("index.html");
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(inputStream));
String str="";
while((str=bufferedReader.readLine())!=null){
response.setBody(str+"n");
}
bufferedReader.close();
}else{
//已經登錄就不需要再登錄的
response.setStatus(200);
response.setMessage("OK");
response.setHeaders("Content-Type","text/html; charset=utf-8");
response.setBody("<html>");
response.setBody("該用戶已經登錄了");
response.setBody("</html>");
}
}
}
public static void main(String[] args) throws IOException {
HttpServerV4 httpServerV4=new HttpServerV4(9090);
httpServerV4.start();
}
}
一個進程可以bind多個端口號
但是一個端口號不能被多個進程bind
四、傳輸層協(xié)議TCP和UDP
1、UDP協(xié)議的理解
對于UDP來說它有三個非常重要的特性
1)無連接(不需要建立連接就可以通信)
2)不可靠(接收方是否接受到消息發(fā)送方不知道)
3)面向數(shù)據(jù)報(以DatagramPacket為單位進行傳輸)
UDP的協(xié)議格式:
2、TCP傳輸協(xié)議
對于TCP來說它有三個非常重要的特性
1)有連接(需要建立連接就可以通信)
2)可靠(接收方是否接受到消息發(fā)送方知道)
3)面向字節(jié)流
TCP的協(xié)議格式:
TCP的報頭是變長(報頭長度在4的基礎上*4)
3、TCP的十個特性
1、確認應答(核心)
當主機a向主機B發(fā)送數(shù)據(jù)的時候,主機B會向主機a發(fā)送一種應答報文ack,當主機a接收到由主機B發(fā)來的ack時,那么就能確定主機a發(fā)送過去的數(shù)據(jù)被主機B接收。
當主機a發(fā)送的數(shù)據(jù)為1到100時,那么主機B返回的確認應答為101,接下來數(shù)據(jù)就要從101開始發(fā)送。
2、超時重傳
確認應答是比較理想的情況,數(shù)據(jù)在傳輸過程中有很大的概率丟包。
當出現(xiàn)丟包狀況時有兩種可能,第1種就是主機a發(fā)送的請求丟失,第2種情況就是主機B發(fā)送的ack丟失,當發(fā)送一條數(shù)據(jù)后,TCP內部就會自動生成一個定時器,當一定的時間沒有收到ack,那么就會觸發(fā)定時器觸發(fā)重傳。如果是ack丟了,那么TCP會在內部進行數(shù)據(jù)去重。
3、連接管理
a)建立連接(三次握手)
三次握手涉及到SYN同步報文段當主機a發(fā)送SYN同步報文段,嘗試與主機B發(fā)生連接,當主機B收到,來自主機a的SYN后,會向主機a發(fā)送SYN和ACK。當主機A收到,來自主機B的數(shù)據(jù)后,會向主機B發(fā)送一個ACK。
注意:
如果三次握手僅只握兩次,那么有一方接收不到信號就不能正常工作。如果三次握手握手四次,可以但是會降低傳輸效率。
幾個重要的狀態(tài):
LISTEN:表示服務器已經準備好了,可以接受客戶端的消息了。
SYN-SEND/SYN-RCVD: 建立連接的中間過程,如果建立連接順利這兩個狀態(tài)一瞬間就會消失。
ESTABLISHED:連接建立完成驗證的雙方都有發(fā)送和接收能力,這時就可以開始傳輸正文了。
b)斷開連接(四次揮手)
四次揮手主機A向主機B發(fā)送FIN,主機B收到由主機A發(fā)來的FIN后就會立即發(fā)送ACK,當應用程序代碼處理完積壓的數(shù)據(jù)后,主機B才會發(fā)送FIN給主機A,主機A接收到由主機B發(fā)來的信息返回ACK。
注意:
當出現(xiàn)延時應答時,四次揮手就會變成三次揮手。
CLOSE_WAIT:四次揮手揮到一半就不揮了,主要原因是因為接受方沒有調用close方法,會導致4次揮手只回了兩次而沒有正確的關閉連接。
TIME_WAIT:誰主動斷開連接,誰就會進入這個狀態(tài)。這時主機已經完成了4次揮手的過程,但是不能立即釋放連接要等待一段時間之后。再徹底釋放連接。
4、滑動窗口
滑動窗口的本質是節(jié)省了時間,當窗口為n時,n份數(shù)據(jù)傳輸?shù)臅r間變成了一份時間,n份應答的時間也重疊成一份時間。
當滑動窗口出現(xiàn)丟包問題時間:
1)ACK丟了
出現(xiàn)這種情況不需要進行處理,當接收方接受到6001時,就會自動默認前面的數(shù)據(jù)發(fā)送過來,這時窗口直接滑動。
2)數(shù)據(jù)丟失
這種情況下,接下來發(fā)送的ACK都會是丟失的那個數(shù)據(jù)的開始,當收到三個后開始重傳,成功后ACK就變成相應的,窗口向后滑動。
5、流量控制
窗口不能無線大,傳輸速率過快會讓接收方處理不來,當處理方沒有空間時,就會停止發(fā)送,這時主機a就會發(fā)送一個窗口探測,通過主機B的不斷更新,從而得知主機a是否繼續(xù)發(fā)送數(shù)據(jù)。
流量控制本質上就是根據(jù)接收方的處理能力來反向橫置發(fā)送能力。根據(jù)接受緩沖區(qū)的剩余空間大小,來約制發(fā)送方的滑動窗口大小。
6、擁塞控制
擁塞控制是在不斷變化的,最終的窗口大小取決于擁塞窗口和流量控制窗口的最小值。
可以根據(jù)網絡狀態(tài)進行及時的調整。
7、延時應答
目的是在流量控制的基礎上,返回一個合理但是比較大的窗口。
延時應答就是讓ack延時發(fā)送一點(延時應答的時間不回超過超時重傳的時間)
8、捎帶應答
ACK和Resp本來不能同時相應,但是由于捎帶應答就可以把兩個操作合并。
9、粘包問題
當數(shù)據(jù)進行讀取的時候,讀取的內容可能和想象的內容有差異,面向字節(jié)流傳輸都會出現(xiàn)這個問題。
解決方法:
我們通過應用層協(xié)議本身來區(qū)分出包和包之間的邊界。(使用分割符、明確包的長度)
10、保活機制
1、進程崩潰時,TCP連接就會進行正常的四次揮手。
2、主機關機時,關機會強制殺死進程,殺死進程就是四次揮手。
3、主機斷電:
a)接收方斷電,對端嘗試發(fā)送消息ACK沒有接受(超時重傳,到達一定次數(shù)重置連接,放棄連接)
b)發(fā)送方斷電,對端嘗試接受消息(在空閑時間會傳輸心跳包,當心跳包沒有相應就會自動放棄)
4、TCP和UDP之間的對比
1、TCP適用于要求可靠性的場景。(外網通信網絡環(huán)境復雜的地方,udp丟包概率大,可以考慮TCP)。
2、Udp適用于對于可靠性要求沒有那么高,但是需要很高的傳輸效率的場景,如機房等地方。
3、Udp能夠實現(xiàn)廣播,但是TCP只能1對1進行傳輸。
4、對于游戲這種領域,udp和TCP都不能傳輸。、
五、網絡層傳輸協(xié)議IP
網絡層解決兩個問題:(地址管理和路由選擇)
報頭長度是在改變的。(不需要手動分包)
4位版本號(version): 指定IP協(xié)議的版本, 對于IPv4來說, 就是4.
4位頭部長度(header length): IP頭部的長度是多少個32bit, 也就是 length * 4 的字節(jié)數(shù). 4bit表示最大的數(shù)字是15, 因此IP頭部最大長度是60字節(jié).
8位服務類型(Type Of Service): 3位優(yōu)先權字段(已經棄用), 4位TOS字段, 和1位保留字段(必須置為0). 4位TOS分別表示: 最小延時, 最大吞吐量, 最高可靠性, 最小成本 . 這四者相互沖突, 只能選擇一個. 對于ssh/telnet這樣的應用程序, 最小延時比較重要; 對于ftp這樣的程序, 最大吞吐量比較重要.
16位總長度(total length): IP數(shù)據(jù)報整體占多少個字節(jié).
16位標識(id): 唯一的標識主機發(fā)送的報文. 如果IP報文在數(shù)據(jù)鏈路層被分片了, 那么每一個片里面的這個id都是相同的.
3位標志字段: 第一位保留(保留的意思是現(xiàn)在不用, 但是還沒想好說不定以后要用到). 第二位置為1表示禁止分片, 這時候如果報文長度超過MTU, IP模塊就會丟棄報文. 第三位表示"更多分片", 如果分片了的話, 最后一個分片置為1, 其他是0. 類似于一個結束標記.
13位分片偏移(framegament offset): 是分片相對于原始IP報文開始處的偏移. 其實就是在表示當前分片在原報文中處在哪個位置. 實際偏移的字節(jié)數(shù)是這個值 * 8 得到的. 因此, 除了最后一個報文之外, 其他報文的長度必須是8的整數(shù)倍(否則報文就不連續(xù)了).
8位生存時間(Time To Live, TTL): 數(shù)據(jù)報到達目的地的最大報文跳數(shù). 一般是64. 每次經過一個路由, TTL -=1, 一直減到0還沒到達, 那么就丟棄了. 這個字段主要是用來防止出現(xiàn)路由循環(huán)
8位協(xié)議: 表示上層協(xié)議的類型
16位頭部校驗和: 使用CRC進行校驗, 來鑒別頭部是否損壞.
32位源地址和32位目標地址: 表示發(fā)送端和接收端
1、地址管理
地址管理就是給每個主機一個單獨的身份標識。
我們有兩種方法:
1)動態(tài)分配IP地址:聯(lián)網就分配不聯(lián)網就不分配。
2)NAT機制,網絡轉換機制,允許局域網中的IP地址可以重復,使用一個外網IP來代表同一批局域網內部設備,一般由路由器負責。
在局域網內我們通過端口號來區(qū)分。
2、網段劃分
網段劃分就是將一個IP地址劃分成 網絡號和主機號 兩個部分。
1、同一個局域網內部的設備,網絡號相同主機號不同
2、兩個相鄰的局域網,網絡號不能相同。
1)將IP地址劃分成ABCDEF五給類別。
2、使用子網掩碼的方法來劃分。
子網掩碼和IP地址按位與就可以得到網絡號
例如:
IPV4地址:192.168.0.16 子網掩碼:255.255.255.0
網絡號:192.168.0.0
相鄰兩個局域網之間的網絡號是不一樣的
主機號為.1的叫做網關,也就是當前局域網的路由器
局域網內部IP:
10.*,前八位是網絡號
172.16.~172.31.,前12位是網絡號
192.168.*,前16位是網絡號
這些IP稱為私有IP,其他的稱為全局IP。
3、路由選擇
當一個包從我的電腦出發(fā),他會首先查看我的電腦認不認識這個目的IP,如果不認識就會發(fā)給我電腦所連接的路由器,有我電腦連接的路由器,認不認識這個目的IP如果認識就會傳輸過去,如果不認識再把數(shù)據(jù)交給光貓,如果光貓也不認識,那么就會繼續(xù)交給上級路由器。
路由表:(和上面所說的差不多)
當有一個IP數(shù)據(jù)達到路由器的時候,路由器就會檢查,看目的IP所屬的網站在路由表中有哪些對應的選項,如果沒有就會走默認選項。
目的IP & 子網掩碼再路由表中查找。
六、數(shù)據(jù)鏈路層和應用層(以太網協(xié)議和DNS協(xié)議)
1、以太網協(xié)議
數(shù)據(jù)鏈路層負責兩個相鄰設備之間的傳輸
以太網協(xié)議覆蓋了數(shù)據(jù)鏈路層和物理層。
面試常見:
已經有IP的情況下為什么還有MAC地址?
1目錄
4、TCP網絡傳輸?shù)幕玖鞒?/p>
二、網絡編程套接字(socket)
5、cookie和session的用法
6、基本實現(xiàn)http協(xié)議的代碼
四、傳輸層協(xié)議TCP和UDP
4、TCP和UDP之間的對比
六、數(shù)據(jù)鏈路層和應用層(以太網協(xié)議和DNS協(xié)議)
一、網絡通信協(xié)議
1、概述
計算機網絡是通過 傳輸介質、通信設施和網絡通信協(xié)議 ,把分散在不同地點的計算機設備互連起來的,實現(xiàn)資源共享和數(shù)據(jù)傳輸?shù)南到y(tǒng)。網絡編程就是編寫程序使互聯(lián)網的兩個(或多個)設備(如計算機)之間進行數(shù)據(jù)傳輸。Java語言對網絡編程提供了良好的支持。通過其提供的接口我們可以很方便地進行網絡編程。
網絡通信協(xié)議是網絡編程中的關鍵,通信雙方達成的一種共識,當通信雙方都遵守這樣的約定,才能正確傳輸信息。
2、為什么協(xié)議要分層?
1、分層可以避免一個協(xié)議太過于龐大和復雜。
2、分層之后協(xié)議之間 解耦合 ,上層協(xié)議不需要理解下層協(xié)議的細節(jié)
3、任意層次的協(xié)議的可以進行靈活的替換
3、對于參考模型的介紹
在這里我們對于TCP/IP模型進行詳細的描述:
應用層:和應用程序直接打交道的協(xié)議。
傳輸層:負責端到端之間的傳輸(關注起點和終點)
網絡層:負責點到點之間的傳輸(關注傳輸?shù)穆窂剑?/h1>
數(shù)據(jù)鏈路層:負責相鄰點之間如何具體傳輸。
物理層:網絡通信的基礎硬件措施。
面試常見:
1、對于一臺主機來說,它的操作系統(tǒng)內核實現(xiàn)了從應用層到物理層
2、對于一臺路由器來說,它實現(xiàn)了從網絡層到物理層。
3、對于一臺交換機來說,它實現(xiàn)了從數(shù)據(jù)鏈路層到物理層。
4、對于集線器來說,它實現(xiàn)的物理層。
4、TCP網絡傳輸?shù)幕玖鞒?/h1>
在這些基本操作中涉及到 封裝 和 分用 兩個操作流程。
封裝的意思就是給基于數(shù)據(jù)的基礎上,在數(shù)據(jù)前加上協(xié)議報頭,當數(shù)據(jù)由應用層傳輸?shù)絺鬏攲訒r,傳輸層會在得到的數(shù)據(jù)基礎上加上傳輸層協(xié)議報頭。 當數(shù)據(jù)由傳輸層到達網絡層時,會繼續(xù)添加網絡層協(xié)議報頭,也就是我們常說的IP地址。當數(shù)據(jù)由網絡層傳輸?shù)綌?shù)據(jù)鏈路層時會在數(shù)據(jù)前加上數(shù)據(jù)鏈路層協(xié)議報頭,也就是常數(shù)的MAC地址。數(shù)據(jù)鏈路層將數(shù)據(jù)發(fā)送給物理層時,物理層就會將這個數(shù)據(jù)轉化成光電信號,通過一些硬件設備,例如網線,光纖,電磁波等傳輸出去。
分用的過程正好與封裝相反,當物理層接收到對方發(fā)送過來的光電信號時,會將它解析成二進制的bit流,進一步得到數(shù)據(jù)鏈路層數(shù)據(jù)幀。 數(shù)據(jù)鏈路層解析數(shù)據(jù)幀,玻璃針頭和針尾取出,其中的IP數(shù)據(jù)報交給網絡層。網絡層接收到剛才的網絡層數(shù)據(jù)報,通過解析去掉網絡層的協(xié)議報頭。把數(shù)據(jù)交給傳輸層,傳輸層拿到傳輸層,數(shù)據(jù)報在進行解析,去掉傳輸層報頭,最后將數(shù)據(jù)交給應用層,這時應用層就可以解析數(shù)據(jù),分析出發(fā)送者是誰,接收者是誰顯示到桌面上。
當發(fā)送者通過自己的主機將數(shù)據(jù)封裝完成之后,會通過路由器廣域網等發(fā)送到服務器。當服務器通過分用把數(shù)據(jù)拆開之后看到接收方的地址,然后服務器將數(shù)據(jù)重新封裝之后,通過路由器廣域網等途徑發(fā)送到接收者的主機,然后接收者的主機一步一步的進行分傭,最終得到數(shù)據(jù)并且顯示出來。
二、網絡編程套接字(socket)
套接字(socket)是一組API,用來實現(xiàn)網絡編程,一般是通過客戶端(server)和服務器(cilent)實現(xiàn)的。
IP地址:用來識別互聯(lián)網上一臺主機的位置。
端口號:用來區(qū)分是主機上的那個應用。
在一次通信過程中涉及到五元組: 源IP , 目的IP , 源端口 , 目的端口 , 協(xié)議類型 。
JAVA中提供了兩種風格:
UDP(DatagramSocket面向數(shù)據(jù)報,發(fā)送和接受數(shù)據(jù)要以數(shù)據(jù)包為單位)
TCP(ServerSocket面向字節(jié)流)
1、UDP(面向數(shù)據(jù)報)
1)UDP的服務器
//UDP的服務器
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class Test01 {
//進行實例化操作
private DatagramSocket socket=null;
//端口號
public Test01(int port) throws SocketException {
socket=new DatagramSocket(port);
}
//開啟服務器
public void start() throws IOException {
System.out.println("開啟服務器");
while (true){
//1、讀取請求并分析
DatagramPacket requstPacket=new DatagramPacket(new byte[4096],4096);
socket.receive(requstPacket);
String reques=new String(requstPacket.getData(),0,
requstPacket.getLength()).trim();
//2、請求數(shù)據(jù)相應
String response=process(reques);
//3、把響應返回給服務器
DatagramPacket responsePacket=new DatagramPacket(response.getBytes()
,response.getBytes().length,requstPacket.getSocketAddress());
socket.send(responsePacket);
//日志
System.out.printf("[%s:%d] req: %s; resp:%s n",requstPacket.getAddress().toString(),
requstPacket.getPort(),reques,response);
}
}
private String process(String reques) {
//我們寫最簡單的服務器回溯
return reques;
}
public static void main(String[] args) throws IOException {
Test01 test01=new Test01(9090);
test01.start();
}
}
2)UDP的客戶端
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class Test02 {
// 客戶端的主要流程分成四步.
// 1. 從用戶這里讀取輸入的數(shù)據(jù).
// 2. 構造請求發(fā)送給服務器
// 3. 從服務器讀取響應
// 4. 把響應寫回給客戶端.
private DatagramSocket socket = null;
private String serverIp;
private int serverPort;
// 需要在啟動客戶端的時候來指定需要連接哪個服務器
public Test02(String serverIp, int serverPort) throws SocketException {
this.serverIp = serverIp;
this.serverPort = serverPort;
socket = new DatagramSocket();
}
public void start() throws IOException {
Scanner scanner = new Scanner(System.in);
while (true) {
// 1. 讀取用戶輸入的數(shù)據(jù)
System.out.print("-> ");
String request = scanner.nextLine();
if (request.equals("exit")) {
break;
}
// 2. 構造請求發(fā)送給服務器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
request.getBytes().length, InetAddress.getByName(serverIp), serverPort);
socket.send(requestPacket);
// 3. 從服務器讀取響應
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(), 0, responsePacket.getLength()).trim();
// 4. 顯示響應數(shù)據(jù)
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
Test02 client = new Test02("127.0.0.1", 9090);
// UdpEchoClient client = new UdpEchoClient("47.98.116.42", 9090);
client.start();
}
}
2、TCP(面向字節(jié)流)
客戶端發(fā)送的數(shù)據(jù)是按行讀取(n)
1)服務器
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test05 {
// 1. 初始化服務器
// 2. 進入主循環(huán)
// 1) 先去從內核中獲取到一個 TCP 的連接
// 2) 處理這個 TCP 的連接
// a) 讀取請求并解析
// b) 根據(jù)請求計算響應
// c) 把響應寫回給客戶端
private ServerSocket serverSocket=null;
public Test05(int port) throws IOException {
//綁定端口號
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服務器啟動");
//創(chuàng)建一個線程池
ExecutorService executorService= Executors.newCachedThreadPool();
while(true){
//先從內核中獲取到TCP連接
Socket clientSocket=serverSocket.accept();
//處理這個連接
//我們可以在這個地方加上一個線程池
executorService.execute(new Runnable() {
@Override
public void run() {
processConnection(clientSocket);
}
});
}
}
private void processConnection(Socket clientSocket) {
//獲取地址和端口
System.out.printf("[%s:%d] 客戶端上線 n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
try(BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))){
//長連接版本服務器
while(true){
//讀取
String request=bufferedReader.readLine();
//處理數(shù)據(jù)
String response=process(request);
//將這個返回給客戶端
bufferedWriter.write(response+"n");
bufferedWriter.flush();
//寫一個日志
System.out.printf("[%s:%d] req:%s,resq:%sn",clientSocket.getInetAddress().toString()
,clientSocket.getPort(),request,response);
}
} catch (IOException e) {
//e.printStackTrace();
System.out.printf("[%s:%d] 客戶端下線n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
Test05 test05=new Test05(9090);
test05.start();
}
}
2)客戶端
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class Test04 {
// 1. 啟動客戶端(一定不要綁定端口號) 和服務器建立連接
// 2. 進入主循環(huán)
// a) 讀取用戶輸入內容
// b) 構造一個請求發(fā)送給服務器
// c) 讀取服務器的響應數(shù)據(jù)
// d) 把響應數(shù)據(jù)顯示到界面上.
private Socket socket=null;
public Test04(String socketIP,int socketPort) throws IOException {
socket=new Socket(socketIP,socketPort);
}
public void start(){
System.out.println("啟動客戶端");
Scanner scanner=new Scanner(System.in);
try(BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) {
while(true){
System.out.print("-> ");
String request=scanner.nextLine();
if(request.equals("exit")){
break;
}
//發(fā)送給服務器
bufferedWriter.write(request+"n");
//刷新 因為寫入到緩沖區(qū)中并沒有寫入到內核中
bufferedWriter.flush();
//讀取服務器中的內容
String response=bufferedReader.readLine();
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
Test04 test04=new Test04("127.0.0.1",9090);
test04.start();
}
}
注意:
bufferedWriter中的write方法可能寫到緩沖區(qū)中沒有寫入內核中,需要使用flush方法來進行刷新。
如果要多個客戶端可以同時相應我們可以通過一個線程池來控制多個客戶端。
三、應用層協(xié)議HPPT
hppt和hppts都是應用層協(xié)議,應用層協(xié)議大多都需要手動指定,hppt協(xié)議是基于TCP來實現(xiàn)的。
1、URL
當你打開一個網頁的時候,會出現(xiàn)一個網址這個網址就是URL。
url中的服務器的ip來確定是哪一個服務器,
url中的端口號來確定是哪個進程。
url中的path來確定是哪個進程所管理的具體文件。
https://www.baidu.com/s? cl=3 &tn=baidutop10 &fr=top1000 &wd=%E6%95%B0%E8%AF%BB%E5%8D%81%E4%B9%9D%E5%B1%8A%E5%85%AD%E4%B8%AD%E5%85%A8%E4%BC%9A%E7%B2%BE%E7%A5%9E &rsv_idx=2 &rsv_dl=fyb_n_homepage &sa=fyb_n_homepage &hisfilter=1
這種鍵值對是特殊約定的事物。
2、抓包(Fiddler)
什么是抓包?我用一張圖來描述
fiddler不會對傳輸數(shù)據(jù)進行加工和修改,只會講數(shù)據(jù)截取下來讓用戶看到。
HPPT請求:
1)首行: 方法(GET) URL 版本號
2)協(xié)議頭(header):若干個鍵值對用(:)來分割
3)空行:header的結束標記
4)正文(body):對于get來說為空,對于post來說不為空
HPPT相應:
1)首行: 版本號 狀態(tài)碼 狀態(tài)碼對應的描述信息
2)協(xié)議頭(header):若干個鍵值對用(:)來分割
3)空行:header的結束標記
4)正文(body):常見的格式為html格式,部分可能存在加密的情況
3、HPPT的方法
GET和POST的區(qū)別?
GET一般把數(shù)據(jù)放到url中去
Post一般把數(shù)據(jù)放到body中去
4、狀態(tài)碼的詳解
5、cookie和session的用法
cookie可以將登錄認證后的用戶信息保存在本地瀏覽器上,當后面每次發(fā)送http請求時,都會附帶上這個信息,就不需要每次都重新驗證用戶。
但是這樣明文傳輸cookie可能會泄密在這個前提下我們引入session來保存數(shù)據(jù)
在服務器登錄成功時,我們把用戶保存到一個hash表中,同時生成一個key(sessionid),把sessionid寫回到這個cookie中去,當后續(xù)訪問時,只需要在通過sessionid訪問服務器中就行了。
6、基本實現(xiàn)http協(xié)議的代碼
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
public class HttpRequset {
private String method;//方法
private String url;//url
private String version;//版本號
//協(xié)議頭
private Map<String,String> headers=new HashMap<>();
//url中和body中的內容放在這里
private Map<String,String> parameters=new HashMap<>();
//cookie中的類容解析
private Map<String,String> cookies=new HashMap<>();
private String body;//body
//用工廠模式來寫
public static HttpRequset build(InputStream inputStream) throws IOException {
HttpRequset requset=new HttpRequset();
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(inputStream));
//1、解析首行
String firstline=bufferedReader.readLine();
String[] fiestlines=firstline.split(" "); //第一行是通過空格來區(qū)分的
requset.method=fiestlines[0];
requset.url=fiestlines[1];
requset.version=fiestlines[2];
//2、將首行中的url中的元素提取出來
int pre=requset.url.indexOf("?");//如果沒有就返回-1
if(pre!=-1){
String qureyString=requset.url.substring(pre+1);
parseKV(qureyString,requset.parameters);
}
//3、將headers中的內容解析出來
String line="";
while((line=bufferedReader.readLine())!=null && line.getBytes().length!=0){
String[] lines=line.split(": ");
requset.headers.put(lines[0],lines[1]);
}
//4、解析cookie中的內容
String cookie=requset.headers.get("Cookie");
if(cookie!=null){
parseCookie(cookie,requset.cookies);
}
//5、body中的內容
if("POST".equalsIgnoreCase(requset.method) || "PUT".equalsIgnoreCase(requset.method)){
//我們首先要知道body的長度
int length=Integer.parseInt(requset.headers.get("Content-Length"));
//建立一個緩沖區(qū)
char[] buffer=new char[length];
int len=bufferedReader.read(buffer);
requset.body=new String(buffer,0,len);
parseKV(requset.body,requset.parameters);
}
return requset;
}
private static void parseCookie(String cookie, Map<String, String> cookies) {
String[] line=cookie.split("; ");
for(String s:line){
String[] lines=s.split("=");
cookies.put(lines[0],lines[1]);
}
}
private static void parseKV(String qureyString, Map<String, String> parameters) {
String[] line=qureyString.split("&");
for (String s:line){
String[] lines=s.split("=");
parameters.put(lines[0],lines[1]);
}
}
public String getMethod() {
return method;
}
public String getUrl() {
return url;
}
public String getVersion() {
return version;
}
public String getBody() {
return body;
}
public String getHeaders(String key){
return headers.get(key);
}
public String getParameters(String key){
return parameters.get(key);
}
public String getCooies(String key){
return cookies.get(key);
}
}
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.HashMap;
import java.util.Map;
public class HttpResponse {
private String version="HTTP/1.1";//版本號
private int status;//驗證信息
private String message;//狀態(tài)信息
//header中的內容
private Map<String,String> headers=new HashMap<>();
//body中的內容
private StringBuffer body=new StringBuffer();
private OutputStream outputStream=null;
//工廠模式
public static HttpResponse build(OutputStream outputStream){
HttpResponse response=new HttpResponse();
response.outputStream=outputStream;
return response;
}
public void setVersion(String version) {
this.version = version;
}
public void setStatus(int status) {
this.status = status;
}
public void setMessage(String message) {
this.message = message;
}
public void setHeaders(String key,String value){
this.headers.put(key,value);
}
public void setBody(String val){
this.body.append(val);
}
public void fush() throws IOException {
BufferedWriter bufferedWriter=new BufferedWriter(new OutputStreamWriter(outputStream));
//寫第一行
bufferedWriter.write(version+" "+status+" "+message+"n");
//將文件信息放入header中寫入
headers.put("Content-Length",body.toString().getBytes().length+"");
//將header中的類容寫入
for(Map.Entry<String,String> entry:headers.entrySet()){
bufferedWriter.write(entry.getKey()+": "+entry.getValue()+"n");
}
//
bufferedWriter.write("n");
//將body中的數(shù)據(jù)放入
bufferedWriter.write(body.toString());
//將數(shù)據(jù)刷新
bufferedWriter.flush();
}
}
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<form method="post" action="/login">
<div style="margin-bottom: 5px">
<input type="text" name="username" placeholder="請輸入姓名">
</div>
<div style="margin-bottom: 5px">
<input type="text" name="password" placeholder="請輸入密碼">
</div>
<div>
<input type="submit" value="登錄">
</div>
</form>
</body>
</html>
import Test05.HttpServerV3;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class HttpServerV4 {
static class User{
//保存的相關信息
public String userName;
public int age;
public String school;
}
private ServerSocket socket=null;
//我們通過session會話
private Map<String,User> session=new HashMap<>();
public HttpServerV4(int port) throws IOException {
this.socket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("系統(tǒng)啟動");
ExecutorService executorService= Executors.newCachedThreadPool();
while(true){
Socket cliensocket=socket.accept();
executorService.execute(new Runnable() {
@Override
public void run() {
process(cliensocket);
}
});
}
}
private void process(Socket cliensocket) {
try {
//讀取請求
HttpResponse response=HttpResponse.build(cliensocket.getOutputStream());
HttpRequset requset=HttpRequset.build(cliensocket.getInputStream());
//response.setHeaders("Set-Cookie","YYX=qwe");
//計算相應
if("GET".equalsIgnoreCase(requset.getMethod())){
doGet(requset,response);
}else if("Post".equalsIgnoreCase(requset.getMethod())){
doPost(requset,response);
}else{
response.setStatus(405);
response.setMessage("Method Not Allowed");
}
//寫入
response.fush();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
cliensocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void doPost(HttpRequset requset, HttpResponse response) {
//2
if(requset.getUrl().startsWith("/login")){
String user=requset.getParameters("username");
String pass=requset.getParameters("password");
/*System.out.println(user+":"+pass);*/
if("yyx".equals(user) && "2001".equals(pass)){
//通過
response.setStatus(403);
response.setMessage("Forbidden");
//要寫不然返回的不是utf-8的編碼格式
response.setHeaders("Content-Type","text/html; charset=utf-8");
//response.setHeaders("Set-Cookie","userName=YYX");
String sessionId= UUID.randomUUID().toString();
User user01=new User();
user01.userName="楊亞軒";
user01.age=18;
user01.school="武昌工學院";
session.put(sessionId,user01);
response.setHeaders("Set-Cookie","sessionId="+sessionId);
response.setBody("<html>");
response.setBody("驗證成功!");
response.setBody("</html>");
}else {
//沒有通過
response.setStatus(403);
response.setMessage("Forbidden");
//要寫不然返回的不是utf-8的編碼格式
response.setHeaders("Content-Type","text/html; charset=utf-8");
response.setBody("<html>");
response.setBody("驗證失敗!");
response.setBody("</html>");
}
}
}
private void doGet(HttpRequset requset, HttpResponse response) throws IOException {
//1
if(requset.getUrl().startsWith("/index.html")){
String sessionId=requset.getCooies("sessionId");
User user=session.get(sessionId);
if (sessionId==null || user==null) {
//當前情況下用戶沒有登錄
response.setStatus(200);
response.setMessage("OK");
response.setHeaders("Content-Type","text/html; charset=utf-8");
InputStream inputStream=HttpServerV3.class.getClassLoader().getResourceAsStream("index.html");
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(inputStream));
String str="";
while((str=bufferedReader.readLine())!=null){
response.setBody(str+"n");
}
bufferedReader.close();
}else{
//已經登錄就不需要再登錄的
response.setStatus(200);
response.setMessage("OK");
response.setHeaders("Content-Type","text/html; charset=utf-8");
response.setBody("<html>");
response.setBody("該用戶已經登錄了");
response.setBody("</html>");
}
}
}
public static void main(String[] args) throws IOException {
HttpServerV4 httpServerV4=new HttpServerV4(9090);
httpServerV4.start();
}
}
一個進程可以bind多個端口號
但是一個端口號不能被多個進程bind
四、傳輸層協(xié)議TCP和UDP
1、UDP協(xié)議的理解
對于UDP來說它有三個非常重要的特性
1)無連接(不需要建立連接就可以通信)
2)不可靠(接收方是否接受到消息發(fā)送方不知道)
3)面向數(shù)據(jù)報(以DatagramPacket為單位進行傳輸)
UDP的協(xié)議格式:
2、TCP傳輸協(xié)議
對于TCP來說它有三個非常重要的特性
1)有連接(需要建立連接就可以通信)
2)可靠(接收方是否接受到消息發(fā)送方知道)
3)面向字節(jié)流
TCP的協(xié)議格式:
TCP的報頭是變長(報頭長度在4的基礎上*4)
3、TCP的十個特性
1、確認應答(核心)
當主機a向主機B發(fā)送數(shù)據(jù)的時候,主機B會向主機a發(fā)送一種應答報文ack,當主機a接收到由主機B發(fā)來的ack時,那么就能確定主機a發(fā)送過去的數(shù)據(jù)被主機B接收。
當主機a發(fā)送的數(shù)據(jù)為1到100時,那么主機B返回的確認應答為101,接下來數(shù)據(jù)就要從101開始發(fā)送。
2、超時重傳
確認應答是比較理想的情況,數(shù)據(jù)在傳輸過程中有很大的概率丟包。
當出現(xiàn)丟包狀況時有兩種可能,第1種就是主機a發(fā)送的請求丟失,第2種情況就是主機B發(fā)送的ack丟失,當發(fā)送一條數(shù)據(jù)后,TCP內部就會自動生成一個定時器,當一定的時間沒有收到ack,那么就會觸發(fā)定時器觸發(fā)重傳。如果是ack丟了,那么TCP會在內部進行數(shù)據(jù)去重。
3、連接管理
a)建立連接(三次握手)
三次握手涉及到SYN同步報文段當主機a發(fā)送SYN同步報文段,嘗試與主機B發(fā)生連接,當主機B收到,來自主機a的SYN后,會向主機a發(fā)送SYN和ACK。當主機A收到,來自主機B的數(shù)據(jù)后,會向主機B發(fā)送一個ACK。
注意:
如果三次握手僅只握兩次,那么有一方接收不到信號就不能正常工作。如果三次握手握手四次,可以但是會降低傳輸效率。
幾個重要的狀態(tài):
LISTEN:表示服務器已經準備好了,可以接受客戶端的消息了。
SYN-SEND/SYN-RCVD: 建立連接的中間過程,如果建立連接順利這兩個狀態(tài)一瞬間就會消失。
ESTABLISHED:連接建立完成驗證的雙方都有發(fā)送和接收能力,這時就可以開始傳輸正文了。
b)斷開連接(四次揮手)
四次揮手主機A向主機B發(fā)送FIN,主機B收到由主機A發(fā)來的FIN后就會立即發(fā)送ACK,當應用程序代碼處理完積壓的數(shù)據(jù)后,主機B才會發(fā)送FIN給主機A,主機A接收到由主機B發(fā)來的信息返回ACK。
注意:
當出現(xiàn)延時應答時,四次揮手就會變成三次揮手。
CLOSE_WAIT:四次揮手揮到一半就不揮了,主要原因是因為接受方沒有調用close方法,會導致4次揮手只回了兩次而沒有正確的關閉連接。
TIME_WAIT:誰主動斷開連接,誰就會進入這個狀態(tài)。這時主機已經完成了4次揮手的過程,但是不能立即釋放連接要等待一段時間之后。再徹底釋放連接。
4、滑動窗口
滑動窗口的本質是節(jié)省了時間,當窗口為n時,n份數(shù)據(jù)傳輸?shù)臅r間變成了一份時間,n份應答的時間也重疊成一份時間。
當滑動窗口出現(xiàn)丟包問題時間:
1)ACK丟了
出現(xiàn)這種情況不需要進行處理,當接收方接受到6001時,就會自動默認前面的數(shù)據(jù)發(fā)送過來,這時窗口直接滑動。
2)數(shù)據(jù)丟失
這種情況下,接下來發(fā)送的ACK都會是丟失的那個數(shù)據(jù)的開始,當收到三個后開始重傳,成功后ACK就變成相應的,窗口向后滑動。
5、流量控制
窗口不能無線大,傳輸速率過快會讓接收方處理不來,當處理方沒有空間時,就會停止發(fā)送,這時主機a就會發(fā)送一個窗口探測,通過主機B的不斷更新,從而得知主機a是否繼續(xù)發(fā)送數(shù)據(jù)。
流量控制本質上就是根據(jù)接收方的處理能力來反向橫置發(fā)送能力。根據(jù)接受緩沖區(qū)的剩余空間大小,來約制發(fā)送方的滑動窗口大小。
6、擁塞控制
擁塞控制是在不斷變化的,最終的窗口大小取決于擁塞窗口和流量控制窗口的最小值。
可以根據(jù)網絡狀態(tài)進行及時的調整。
7、延時應答
目的是在流量控制的基礎上,返回一個合理但是比較大的窗口。
延時應答就是讓ack延時發(fā)送一點(延時應答的時間不回超過超時重傳的時間)
8、捎帶應答
ACK和Resp本來不能同時相應,但是由于捎帶應答就可以把兩個操作合并。
9、粘包問題
當數(shù)據(jù)進行讀取的時候,讀取的內容可能和想象的內容有差異,面向字節(jié)流傳輸都會出現(xiàn)這個問題。
解決方法:
我們通過應用層協(xié)議本身來區(qū)分出包和包之間的邊界。(使用分割符、明確包的長度)
10、保活機制
1、進程崩潰時,TCP連接就會進行正常的四次揮手。
2、主機關機時,關機會強制殺死進程,殺死進程就是四次揮手。
3、主機斷電:
a)接收方斷電,對端嘗試發(fā)送消息ACK沒有接受(超時重傳,到達一定次數(shù)重置連接,放棄連接)
b)發(fā)送方斷電,對端嘗試接受消息(在空閑時間會傳輸心跳包,當心跳包沒有相應就會自動放棄)
4、TCP和UDP之間的對比
1、TCP適用于要求可靠性的場景。(外網通信網絡環(huán)境復雜的地方,udp丟包概率大,可以考慮TCP)。
2、Udp適用于對于可靠性要求沒有那么高,但是需要很高的傳輸效率的場景,如機房等地方。
3、Udp能夠實現(xiàn)廣播,但是TCP只能1對1進行傳輸。
4、對于游戲這種領域,udp和TCP都不能傳輸。、
五、網絡層傳輸協(xié)議IP
網絡層解決兩個問題:(地址管理和路由選擇)
報頭長度是在改變的。(不需要手動分包)
4位版本號(version): 指定IP協(xié)議的版本, 對于IPv4來說, 就是4.
4位頭部長度(header length): IP頭部的長度是多少個32bit, 也就是 length * 4 的字節(jié)數(shù). 4bit表示最大的數(shù)字是15, 因此IP頭部最大長度是60字節(jié).
8位服務類型(Type Of Service): 3位優(yōu)先權字段(已經棄用), 4位TOS字段, 和1位保留字段(必須置為0). 4位TOS分別表示: 最小延時, 最大吞吐量, 最高可靠性, 最小成本 . 這四者相互沖突, 只能選擇一個. 對于ssh/telnet這樣的應用程序, 最小延時比較重要; 對于ftp這樣的程序, 最大吞吐量比較重要.
16位總長度(total length): IP數(shù)據(jù)報整體占多少個字節(jié).
16位標識(id): 唯一的標識主機發(fā)送的報文. 如果IP報文在數(shù)據(jù)鏈路層被分片了, 那么每一個片里面的這個id都是相同的.
3位標志字段: 第一位保留(保留的意思是現(xiàn)在不用, 但是還沒想好說不定以后要用到). 第二位置為1表示禁止分片, 這時候如果報文長度超過MTU, IP模塊就會丟棄報文. 第三位表示"更多分片", 如果分片了的話, 最后一個分片置為1, 其他是0. 類似于一個結束標記.
13位分片偏移(framegament offset): 是分片相對于原始IP報文開始處的偏移. 其實就是在表示當前分片在原報文中處在哪個位置. 實際偏移的字節(jié)數(shù)是這個值 * 8 得到的. 因此, 除了最后一個報文之外, 其他報文的長度必須是8的整數(shù)倍(否則報文就不連續(xù)了).
8位生存時間(Time To Live, TTL): 數(shù)據(jù)報到達目的地的最大報文跳數(shù). 一般是64. 每次經過一個路由, TTL -=1, 一直減到0還沒到達, 那么就丟棄了. 這個字段主要是用來防止出現(xiàn)路由循環(huán)
8位協(xié)議: 表示上層協(xié)議的類型
16位頭部校驗和: 使用CRC進行校驗, 來鑒別頭部是否損壞.
32位源地址和32位目標地址: 表示發(fā)送端和接收端
1、地址管理
地址管理就是給每個主機一個單獨的身份標識。
我們有兩種方法:
1)動態(tài)分配IP地址:聯(lián)網就分配不聯(lián)網就不分配。
2)NAT機制,網絡轉換機制,允許局域網中的IP地址可以重復,使用一個外網IP來代表同一批局域網內部設備,一般由路由器負責。
在局域網內我們通過端口號來區(qū)分。
2、網段劃分
網段劃分就是將一個IP地址劃分成 網絡號和主機號 兩個部分。
1、同一個局域網內部的設備,網絡號相同主機號不同
2、兩個相鄰的局域網,網絡號不能相同。
1)將IP地址劃分成ABCDEF五給類別。
2、使用子網掩碼的方法來劃分。
子網掩碼和IP地址按位與就可以得到網絡號
例如:
IPV4地址:192.168.0.16 子網掩碼:255.255.255.0
網絡號:192.168.0.0
相鄰兩個局域網之間的網絡號是不一樣的
主機號為.1的叫做網關,也就是當前局域網的路由器
局域網內部IP:
10.*,前八位是網絡號
172.16.~172.31.,前12位是網絡號
192.168.*,前16位是網絡號
這些IP稱為私有IP,其他的稱為全局IP。
3、路由選擇
當一個包從我的電腦出發(fā),他會首先查看我的電腦認不認識這個目的IP,如果不認識就會發(fā)給我電腦所連接的路由器,有我電腦連接的路由器,認不認識這個目的IP如果認識就會傳輸過去,如果不認識再把數(shù)據(jù)交給光貓,如果光貓也不認識,那么就會繼續(xù)交給上級路由器。
路由表:(和上面所說的差不多)
當有一個IP數(shù)據(jù)達到路由器的時候,路由器就會檢查,看目的IP所屬的網站在路由表中有哪些對應的選項,如果沒有就會走默認選項。
目的IP & 子網掩碼再路由表中查找。
六、數(shù)據(jù)鏈路層和應用層(以太網協(xié)議和DNS協(xié)議)
1、以太網協(xié)議
數(shù)據(jù)鏈路層負責兩個相鄰設備之間的傳輸
以太網協(xié)議覆蓋了數(shù)據(jù)鏈路層和物理層。
面試常見:
已經有IP的情況下為什么還有MAC地址?
1、源mac會隨著設備的不同而改變而源IP不會變
2、mac地址和IP地址是獨立發(fā)明從來的
2、DNS協(xié)議
DNS協(xié)議是一套系統(tǒng)(由于IP地址不好記,DNS系統(tǒng)會將域名自動翻譯成IP地址)
瀏覽器的請求可能直接達到cdn服務器就直接返回了。當cdn服務器上沒有時,就會在反向代理服務器緩存中查找,如果反向代理服務器中也沒有,就需要訪問百度的應用服務器。、源mac會隨著設備的不同而改變而源IP不會變
2、mac地址和IP地址是獨立發(fā)明從來的
2、DNS協(xié)議
DNS協(xié)議是一套系統(tǒng)(由于IP地址不好記,DNS系統(tǒng)會將域名自動翻譯成IP地址)
瀏覽器的請求可能直接達到cdn服務器就直接返回了。當cdn服務器上沒有時,就會在反向代理服務器緩存中查找,如果反向代理服務器中也沒有,就需要訪問百度的應用服務器。