目录
前言
这篇博客介绍的是计算机网络的一些内容!!!
网络初识
认识协议
协议就是一种约定,发送方和接收方约定好,按照特定的格式来进行传输
协议分层
上层协议调用下层协议,下层协议给上层协议提供服务,不能隔层调用
TCP/IP
应用层:应用程序 |
传输层:端到端的传输 |
网络层:点到点的传输 |
数据链路层:相邻节点之间的传输 |
物理层:底层基础设施 |
封装和分用
封装的过程中加入本层使用的协议以及关于协议的关键内容
从下到上进行拆包,区分不同的协议进行分用处理。分用就是封装的逆过程。
网络编程套接字
socket套接字
由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元
TCP和UDP
TCP | UDP |
有连接 | 无连接 |
可靠传输 | 不可靠传输 |
面向字节流 | 面向数据报 |
有接收缓冲区,也有发送缓冲区 | 有接收缓冲区,无发送缓冲区 |
大小不限 | 大小受限:一次最多传输64k |
全双工 | 全双工 |
全双工:一个通道,双向通信
半双工:一个通道,单向通信
UDP
DatagramSocket 构造方法
方法签名 | 方法说明 |
DatagramSocket()
|
创建一个
UDP
数据报套接字的
Socket
,绑定到本机任意一个随机端口(一般用于客户端)
|
DatagramSocket(int
port)
|
创建一个
UDP
数据报套接字的
Socket
,绑定到本机指定的端口(一般用于服务端)
|
DatagramSocket 方法(代表一个UDP数据报,也就是一次发送/接收的基本单位)
方法签名 | 方法说明 |
void
receive(DatagramPacket p)
|
从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)
|
void send(DatagramPacket
p)
|
从此套接字发送数据报包(不会阻塞等待,直接发送)
|
void close()
|
关闭此数据报套接字
|
DatagramPacket 构造方法
方法签名
| 方法说明 |
DatagramPacket(byte[] buf, int length) |
构造一个
DatagramPacket
以用来接收数据报,接收的数据保存在 字节数组(第一个参数buf
)中,接收指定长度(第二个参数length)
|
DatagramPacket(byte[]
buf, int offset, int length,
SocketAddress address)
|
构造一个
DatagramPacket
以用来发送数据报,发送的数据为字节 数组(第一个参数buf
)中,从
0
到指定长度(第二个参数length)。
address
指定目的主机的
IP
和端口号
|
DatagramPacket方法
方法签名 | 方法说明 |
InetAddress
getAddress()
|
从接收的数据报中,获取发送端主机
IP
地址;或从发送的数据报中,获取接收端主机IP
地址
|
int getPort()
|
从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号
|
byte[] getData()
|
获取数据报中的数据
|
InetSocketAddress构造方法
方法签名 | 方法说明 |
InetSocketAddress(InetAddress addr, int port)
|
创建一个
Socket
地址,包含
IP
地址和端口号
|
一个最简单的UDP版本的客户端服务器程序:回显服务器
服务器
public class UdpEchoServer {
DatagramSocket socket=null;
//参数的端口表示服务器要绑定的端口
public UdpEchoServer(int port) throws SocketException {
socket=new DatagramSocket(port);
}
//通过这个方法来启动服务器
public void start() throws IOException {
System.out.println("服务器启动!");
while (true){
//循环里面处理一次请求
//1.读取请求并解析
DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
//把这个DatagramPacket对象转成字符串,方便打印
String request=new String(requestPacket.getData(),0, requestPacket.getLength());
//2.根据请求计算响应
String response=process(request);
//3.把响应写回到客户端
DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
//4.打印一个日志,记录当前情况
System.out.printf("[%s:%d] req:%s;resp:%s\n",requestPacket.getAddress().toString(),
requestPacket.getPort(),request,response);
}
}
//当前写的是一个回显服务器
//响应数据就和请求时一样的
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server=new UdpEchoServer(9090);
server.start();
}
}
客户端
public class UdpEchoClient {
DatagramSocket socket=null;
private String serverIp;
private int serverPort;
public UdpEchoClient(String serverIp,int serverPort) throws SocketException {
socket=new DatagramSocket();
this.serverIp=serverIp;
this.serverPort=serverPort;
}
public void start() throws IOException {
Scanner scanner=new Scanner(System.in);
while (true){
//1、从控制台读取用户输入的内容
System.out.print("->");
String request=scanner.next();
//2、构造一个UDP请求,发送给服务器
DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(this.serverIp),this.serverPort);
socket.send(requestPacket);
//3、从服务器上读取UDP数据,并解析
DatagramPacket responsePacket=new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
String response=new String(responsePacket.getData(),responsePacket.getData().length);
//4、把服务器的响应显示到控制台上
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client=new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
DatagramPacket的构造方法
构造空的packet,不需要指定发送给谁 DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);构造有数据的packet,使用InetAddress描述发送给谁 DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),response.getBytes().length, requestPacket.getSocketAddress());构造有数据的packet,使用ip和端口来描述发送给谁 DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(this.serverIp),this.serverPort);
对于服务器来说,这三个步骤:读取请求并解析,根据请求计算响应,把响应写回给客户端,执行速度极快,这个时候如果有多个客户端发来请求,服务器也是可以响应的,但是在服务器这里本质上是三个请求串行处理的
Tcp
给服务器端使用的类
socketserver构造方法
方法签名 | 方法说明 |
ServerSocket(int port)
|
创建一个服务端流套接字
Socket
,并绑定到指定端口
|
socketserver方法
方法签名 | 方法说明 |
Socket accept()
|
开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端
Socket对象,并基于该Socket
建立与客户端的连接,否则阻塞等待
|
void close()
| 关闭此套接字 |
既给服务器端使用,也给客户端用
socket构造方法
方法签名 | 方法说明 |
Socket(String host, int port)
|
创建一个客户端流套接字
Socket
,并与对应
IP
的主机上,对应端口的进程建立连接
|
socket方法
方法签名 | 方法说明 |
InetAddress getInetAddress()
|
返回套接字所连接的地址
|
InputStream getInputStream()
|
返回此套接字的输入流
|
OutputStream getOutputStream()
|
返回此套接字的输出流
|
服务器
public class TcpEchoServer {
// 代码中会涉及到多个 socket 对象. 使用不同的名字来区分.
private ServerSocket listenSocket = null;
public TcpEchoServer(int port) throws IOException {
listenSocket = new ServerSocket(port);
}
public void start() throws IOException {
ExecutorService service=Executors.newCachedThreadPool();
System.out.println("服务器启动!");
while (true){
//1、先用accept接收客户端的连接
Socket clientSocket=listenSocket.accept();
//2、处理这个连接,使用多线程,每个客户端连上来都分配一个新的线程负责处理
// Thread t=new Thread(()->{
// try {
// processConnection(clientSocket);
// } catch (IOException e) {
// e.printStackTrace();
// }
// });
// t.start();
service.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
// 接下来就处理客户端的请求了.
try (InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
while (true) {
// 1. 读取请求并解析.
Scanner scanner = new Scanner(inputStream);
if (!scanner.hasNext()) {
// 读完了, 连接可以断开了.
System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort());
break;
}
String request = scanner.next();
// 2. 根据请求计算响应
String response = process(request);
// 3. 把响应写回给客户端
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response);
// 刷新缓冲区确保数据确实是通过网卡发送出去了.
printWriter.flush();
System.out.printf("[%s:%d] req: %s; resp: %s\n", clientSocket.getInetAddress().toString(),
clientSocket.getPort(), request, response);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 为啥这个地方要关闭 socket ? 而前面的 listenSocket 以及 udp 程序中的 socket 为啥就没 close??
clientSocket.close();
}
}
public String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
客户端
public class TcpEchoClient {
// 客户端需要使用这个 socket 对象来建立连接.
private Socket socket = null;
public TcpEchoClient(String serverIP, int serverPort) throws IOException {
// 和服务器建立连接. 就需要知道服务器在哪了.
// 这里和上节课写的 UDP 客户端差别较大了.
socket = new Socket(serverIP, serverPort);
}
public void start() {
Scanner scanner = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
while (true) {
// 1. 从控制台读取数据, 构造成一个 请求
System.out.print("-> ");
String request = scanner.next();
// 2. 发送请求给服务器
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(request);
// 这个 flush 不要忘记. 否则可能导致请求没有真发出去.
printWriter.flush();
// 3. 从服务器读取响应
Scanner respScanner = new Scanner(inputStream);
String response = respScanner.next();
// 4. 把响应显示到界面上
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
client.start();
}
}
网络原理
应用层
1、HTTP
后续会重点讲解
2、XML
格式是通过“标签”的形式来组织键值对数据的
标签名字就是key,标签里的内容就是value
缺点:数据多了编写复杂;这些数据要通过网络传输,消耗网络带宽
3、JSON
首先是一个{},{}里面包含了多组键值对,键值对之间使用,来分隔;键和值之间,使用:分隔;键只能是字符串类型;值可以是字符串,数字,数组,json
优点:可读性好;美观整洁;扩展性强
缺点:引入额外的字符串,传输数据量变大了,消耗更多的带宽
TCP基本特性
确认应答机制
TCP保证可靠性的最核心机制
报文的序号是1,报文的长度是1000,TCP报头里只能存一个序号,最后一个字节的序号,是根据报文长度来算出来的
确诊应答报文中的确认序号是1001,表达的含义是,1001之前的数据
超时重传机制
确认应答描述的是,数据报顺利到达对方,对方给了个响应,但是在传输过程中,如果丢包,就得进行超时重传
主机A在一个特定时间间隔内没有收到B发来的确认应答,就会进行重发
主机A未收到B发来的确认应答,也可能是因为ACK丢失了
连接管理机制(安全机制)
TCP建立连接(双方建立一个互相认同的关系)-三次握手
TCP断开连接(双方取消相互认同的关系)
通信双方,各自向对方申请断开连接,在各自给对方回应
滑动窗口(效率机制)
为了提高TCP传输效率的有效机制,每次批量的发送一波消息,然后再等一波ACK,再发一波消息
流量控制
在滑动窗口的基础之上,对发送速率做出限制的机制,,就是限制发送方的窗口大小不要太大
接收方使用接收缓冲区的剩余空间的大小,来作为发送方发送速率(窗口大小)的参考数值
拥塞控制
TCP引入慢启动机制,先发少量的数据,发送之初,网络情况前路未知,如果不丢包,就要放大拥塞窗口(拥塞控制下的那个窗口大小),开始的时候先指数增长(翻倍),达到阈值,就线性增长
延迟应答
提高传输效率的机制,又是基于流量控制,来引入的提高效率的机制
捎带应答
确认应答必须等到应用处理完数据并将作为回执的数据返回为止,才能进行捎带应答
面向字节流
面向字节流存在一个典型的问题-“粘包问题”
解决粘包问题,就是要在应用层协议进行区分,只要在定义应用层数据协议的时候,明确包和包之间的边界
1、xml;分隔符就相当于结束标签
2、json;分隔符就相当于}
3、protobuffer;里面通过声明长度的方式来确定边界
4、http;分隔符和长度两个都会用到
网络层
网络层所做的工作,就是在两点之间,规划出一个合理的路径,同时也需要对主机所处的位置,进行定义
1、地址管理;2、路由选择
IP地址的扩展:
1、动态分配IP地址
2、NAT机制
不在强制要求,每个主机都有独立的IP,把IP分成两大类:外网IP/公网IP;内网IP/私网IP/局域网IP
IP协议
网络号:标识网段,保证相互连接的两个网段(网络号)具有不同的标识
主机号:标识主机,同一网段内,主机之间具有相同的网络号,但是必须要有不同的主机号
同一局域网中,主机之间的网络号是相同的,主机号必须不同;在相邻的两个局域网中,要求网络号是不同的(同一路由器连接的)