一、计算机网络的相关概念
1.计算机网络
通过传输介质、网络协议和通信设施,将分散在不同位置的计算机互连,实现资源共享和数据传输的系统。
计算机网络的功能:
1.资源共享
2.信息传输与集中处理
3.均衡负荷与分布处理
4.综合信息服务
2.网络编程
又称Socket编程,是指在操作系统,网络管理软件,网络通信协议的管理和协调下,使用计算机编程语言来实现计算机之间的资源共享和信息传递。
二、计算机网络的三要素:
1.IP地址
IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址。指的是计算机在网络中的唯一标识,IP地址的长度为32个比特位(4字节),一般用“点分十进制”表示。
分类 | 首字节开始位 | 首字节数值范围 | 网络格式地址 | 最大网络个数 | 每个网络最多主机个数 |
---|---|---|---|---|---|
A类 | 0 | 0-127 | 网络.主机.主机.主机 | 127 | 16777214 |
B类 | 10 | 128-191 | 网络.网络.主机.主机 | 16384 | 65534 |
C类 | 110 | 192-223 | 网络.网络.网络.主机 | 2097152 | 254 |
D类 | 1110 | 224-239 | 用于在 IP 网络中的组播,不再分配 | ||
E类 | 1111 | 240-255 | 保留作研究之用,不再分配 |
2.端口号:
端口号用于标识进程的逻辑地址;其有效端口的范围是从 0到65535,其中 0-1024 系统使用或保留端口。注意:不同的软件通信,端口号不能相同,不能有冲突。
端口号分类 | 说明 |
---|---|
公共端口 | 0-1023,这些端口是给操作系统保留的端口号,紧密绑定于一些服务,一般不使用 |
注册端口 | 1024-49151,这些端口可以使用,但不排除有系统占用的情况 |
私有端口 | 49152-65535,给用户使用的端口号 |
3.协议
网络上所有设备之间通信规则的集合,它规定了通信时信息必须采用的格式和这些格式的意义,以及数据的传输方式等。
常用协议和端口号有:
协议名 | 协议说明 | 端口号 |
---|---|---|
FTP | 文件传输协议 | 21 |
HTTP | 超文本传输协议 | 80 |
HTTPS | 加密的超文本传输协议 | 443 |
POP3 | 接收邮件协议 | 110 |
SMAP | 发送邮件协议 | 25 |
IMAP | 互联网邮件访问协议 | 143 |
DNS | 域名解析 | 53 |
4.资源查找方式
通过IP地址找计算机,通过端口号找软件,通过协议来约定数据传输的格式。
三、网络模型
举个栗子:A向B发送一条“helloworld”讯息,根据使用协议(TCP/UDP)的不同,需要经过n层模型,步步转化,直至该讯息“helloworld”被对方接收。
计算机网络之间以何种规则进行通信,就是网络模型所研究的问题。实际的含义是建立某种模型,实现网络数据的传输,中间会对数据进行多次的封装和拆封,实现最底层的传输要求。
网络模型一般是指 OSI 七层参考模型和 TCP/IP 四层参考模型。
1.OSI模型将网络划分为7层,从下到上每一层的含义具体如下:
- 物理层
主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。
主要作用是将数据最终编码为用 0、1 标识的比特流,通过物理介质传输。这一层的数据叫做比特。 - 数据链路层
主要提供介质访问和链路管理,该层将接收到的数据进行 MAC 地址(网卡地址)的封装与解封装。常把这一层的数据叫做帧。这一层常工作的设备是交换机。 - 网络层:
主要进行IP选址及路由选择,该层将接收到的数据进行 IP 地址的封装与解封装。常把这一层的数据叫做数据包。这一层设备是路由器。 - 传输层:
定义了一些数据传输的协议和端口号。主要建立、管理和维护端到端的连接,该层将接收的数据进行分段和传输,到达目的地址后在进行重组。常把这一层的数据叫做段。 - 会话层:
通过传输层建立数据传输的通路,然后建立、管理并维护对话。主要在系统之间发起会话或者接收会话请求。 - 表示层:
主要进行对接收数据的解释、格式转化、加密与解密、压缩与解压缩。确保一个系统的应用层发送的数据能被另一个系统的应用层识别。 - 应用层:
主要是为一些终端应用程序提供服务。 直接面对着用户的。
2.TCP/IP协议簇则将网络划分为4层,从下到上每一层的含义具体如下:
- 网络接口层:建立,维护,断开物理连接
- 网络层:处理分组在网络中的活动
- 传输层:为两台主机上的应用程序提供端到端的通信
- 应用层:负责处理特定的应用程序细节
四、TCP协议
1.TCP报文段首部及各字段的含义
- 源端口和目的端口:各16个比特位,每个TCP报文段都包含源端口和目的端口,用于寻找发送端和接收端的应用进程。一般来讲,端口号和IP地址可以唯一确定一个TCP连接。
- 序号:32个比特位,用来标识从发送端向接收端发送的数据字节流。
- 确认序号:占32个比特位,包含目标端所期望收到源段的下一个数据字节。只有ACK标志位1时,确认序号字段才有效。
- 数据偏移:4个比特位,用于指出TCP首部的长度,若不存在该值,默认首部长度为20个字节,数据偏移的最大值为60个字节,最大可扩充40个字节
- 标志位:各1个比特位
①URG:“紧急指针”有效
②ACK:“确认序号”有效
③PSH:接收方直接将当前包做紧急处理:传递给应用层
④RST:丢弃该包并重新连接
⑤SYN:建立连接时用来同步序号
⑥FIN:释放连接 - 窗口:16个比特位, 用于流量控制和拥塞控制,表示当前接收缓冲区的大小。在计算机网络中,通常是用接收方的接收能力大小来控制发送发的数据发送量。所以当前窗口表示的最大值是65535。
- 检验和:16个比特位,范围覆盖了整个的TCP报文段(首部和数据)。这是一个强制性的字段,必须由发送端计算和存储,由接收端验证。
2.TCP建立连接:三次握手过程
其中:seq是数据包本身的序列号;ack是期望对方继续发送的那个数据包的序列号。
过程简述:
- 客户端发送连接请求报文段到服务端,并进入SYN-SENT状态,等待服务端确认
- 服务端收到连接请求报文,若服务端同意建立连接,会向客户端发送确认报文段,并为该TCP连接分配TCP缓存和变量
- 客户端收到服务端的确认报文段后,向服务端发送确认报文段,并也给该连接分配缓存和变量。此包发送完毕后,客户端和服务端进入ESTABLISHED状态,完成三次握手,至此TCP连接成功。
3.连接的释放:四次挥手过程
过程简述:
- TCP客户端发送一个FIN,用来关闭客户端到服务端的数据传送
- 服务端收到这个FIN,它发回一个ACK,确认序号为收到的序号+1,和SYN一样,一个FIN将占用一个序号
- 服务端关闭客户端连接,发送一个FIN给客户端
- 客户端发回ACK报文进行确认,并将确认序号设置为收到的序号+1
为什么A要先进入TIME-WAIT状态,等待2MSL时间后才进入CLOSED状态? 为了保证B能收到A的确认应答。
若A发完确认应答后直接进入CLOSED状态,那么如果该应答丢失,B等待超时后就会重新发送连接释放请求,但此时A已经关闭了,不会作出任何响应,因此B永远无法正常关闭。
TCP 和 UDP 的区别:
TCP 面向连接(建立连接前会进行三次握手),UDP 面向无连接
TCP可靠,UDP不可靠
TCP 面向字节流,UDP 基于数据报 (将应用层的消息分别打包,以报文式传输到接收端)
TCP 保证数据正确性,UDP 可能丢包
头部提供的信息不同,UDP没有连接服务、重传功能
五、IO模型
介绍IO模型前需要明白【同步/异步】、【阻塞/非阻塞】的概念:
(1)同步/异步:针对应用程序和内核空间的交互而言。
同步指用户进程触发IO操作并等待或者轮询地去查看IO操作是否就绪,而异步是指用户进程触发IO操作以后就可以做自己的事情,当IO操作已经完成时,用户会得到IO操作完成的通知。
(2)阻塞/非阻塞:针对进程在访问数据时,根据IO操作的就绪状态不同采取不同方式,是一种读取/写入操作函数的实现方式。> 阻塞方式下读取或者写入函数将一直等待,非阻塞方式下,读取或者写入函数会立即返回一个状态值。
1.同步阻塞模型
举个栗子:小明去餐馆,点了一个番茄炒蛋,然后就一直在位置上等着菜做好,自己端到餐桌吃饭。这就是同步阻塞,在厨师做饭的时间里,小明需要一直等待。
在此种方式下,用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。JAVA传统的IO模型属于此种方式。
2.同步非阻塞模型
小明去餐馆,点了一个番茄炒蛋,等吃饭。估计时间差不多了,就去问老板菜做好了没有,如果菜好了就去端,没好就等一会再去问,如此循环直到菜做好小明吃饭。这就是同步非阻塞,在向老板问餐的间隔里,小明可以做自己的事情。
在此种方式下,用户进程发起一个IO操作以后边可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费。其中目前JAVA的NIO就属于同步非阻塞IO。
3.IO复用模型
还是小明,点了几个菜,然后跟叫菜员说:“哪个菜好了的话,你跟我说一声,我来拿。”这就是IO复用模型,在叫菜员叫到小明之前,小明可以做自己的事情。
在此种方式下,用户进程需要处理多个IO。IO复用的过程分为两步:第一次系统调用用于请求数据,直到数据复制到内存缓冲区;第二次还需要发起系统调用来同步数据, 这一段依然是阻塞的。虽然IO复用还是会导致阻塞,但是当IO操作对象较多时,就能够避免产生很大一部分的阻塞时间。
为什么说是阻塞的呢?
因为此时是通过select系统调用来完成的,而select函数本身的实现方式是阻塞的,
而采用select函数有个好处就是它可以同时监听多个文件句柄,从而提高系统的并发性
4.信号驱动模型
又是小明,去餐馆,点了几个菜,然后跟老板说:“哪个菜好了的话,你跟我说一声,我来拿。”这就是信号驱动的IO模型,在老板叫到小明之前,小明可以做自己的事情。
在此种方式下,用户可以让内核在描述符就绪时发送SIGIO信号通知我们。使用这种模型,我们需要首先开启套接字的信号驱动式IO功能,并通过sigaction系统调用来安装一个信号处理函数。
5.异步阻塞模型
这次小明点菜不想自己去拿了,他告诉老板,菜好了的话你直接端过来,或者小明直接点外卖送到家里。
在此种方式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。目前Java中还没有支持此种IO模型。
对比信号驱动IO,异步IO的主要区别在于:
信号驱动由内核告诉我们何时可以开始一个IO操作(数据在内核缓冲区中),而异步IO则由内核通知IO操作何时已经完成(数据已经在用户空间中)。
六、网络编程模型
基于上述五种IO模型,在Java中,随着NIO和AIO的引入,一般具有以下几种网络编程模型:BIO、NIO、AIO
1.BIO同步阻塞模型
在JDK1.4之前,我们建立网络连接的时候采用BIO模型,以流的方式处理数据(效率低)。
BIO实现C/S的大致过程:
①在服务端启动一个ServerSocket实例
②在客户端启动Socket实例来对服务端进行通信
默认情况下服务端需要对每个请求建立一堆线程等待请求,而客户端发送请求后,先咨询服务端是否有线程相应,如果没有则会一直等待或者遭到拒绝请求,如果有的话,客户端会线程会等待请求结束后才继续执行。
可见其服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器需要启动一个线程进行处理,如果这个链接不做任何事情会造成不必要的线程开销。
- 服务端编程
import java.io.*;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
/**
* BIO服务端编程
*/
public class BIOEchoServerDemo {
public static void main(String[] args) {
try {
//创建ServerSocket实例
ServerSocket serverSocket = new ServerSocket();
//绑定端口
serverSocket.bind(new InetSocketAddress(8889));
System.out.println("服务端已启动");
//监听并接收该客户端的连接
Socket socket = serverSocket.accept();
System.out.println("客户端"+socket.getRemoteSocketAddress()+"连接上服务器");
//读写业务逻辑
InputStream inputStream = socket.getInputStream();
//读取客户端的消息,存储到reader中
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
//回复客户端消息
OutputStream outputStream = socket.getOutputStream();
String msg;
while ((msg = reader.readLine()) != null) {
System.out.println(socket.getRemoteSocketAddress() + ":" + msg);
outputStream.write((msg + "\n").getBytes());
outputStream.flush();
}
//关闭资源
reader.close();
socket.close();
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 客户端编程
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Scanner;
/**
* BIO客户端编程
*/
public class BIOEchoClientDemo {
public static void main(String[] args) {
try {
//创建socket实例
Socket socket = new Socket();
//连接服务端
socket.connect(new InetSocketAddress("127.0.0.1",8889));//和服务端保持一致
//读写操作
OutputStream outputStream = socket.getOutputStream();
//接收服务端返回的消息
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
Scanner scanner = new Scanner(System.in);
String sendMsg;
while(scanner.hasNext()){
sendMsg=scanner.next();
if(sendMsg!=null) {
//给服务端发送消息
outputStream.write((sendMsg+"\n").getBytes());
//等待服务端返回的内容
String recvMsg = reader.readLine();
System.out.println("[echo];" + recvMsg);
}
}
//关闭资源
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.NIO同步非阻塞模型
NIO是对BIO的改进,是同步非阻塞的IO模型,以块的方式处理数据(效率高)。NIO基于通道和缓冲区进行数据操作,数据总是从通道读取到缓冲区中,或者从缓冲区中写到通道中。Selector(选择器)用于监听多个通道的时间,(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
NIO实现C/S的大致流程:
客户端:
①通过SocketChannel连接远程服务器
②创建读数据/写数据缓冲区对象来读取服务端数据或向服务端发送数据
③关闭SocketChannel
服务端:
①通过ServerSocketChannel绑定ip地址和端口号
②通过ServerSocketChannelImpl的accept()方法创建一个SocketChannel对象用户从客户端读/写数据
③创建读写数据缓冲区对象来读取客户端数据或向客户端发送数据
④关闭SocketChannel和ServerSocketChannel
- 服务端编程
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* NIO服务端编程
*/
public class NIOServerDemo {
public static void main(String[] args) {
try {
//创建ServerSocketChannel通道实例
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定端口
serverSocketChannel.bind(new InetSocketAddress(8889));
System.out.println("服务端已启动");
//将serverSocketChannel设置为非阻塞
serverSocketChannel.configureBlocking(false);//true:阻塞;false:非阻塞
//实例化io复用器
Selector selector = Selector.open();
//将serverSocketChannel注册到复用器上并监听accept事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//TODO
int num;
//监听复用器中是否有事件,select()会处于阻塞状态,直至有事件完成才返回
while ((num = selector.select()) > 0) {
//获取监听事件的结果集,包含已完成事件的集合
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
//若有可接收事件完成
if (selectionKey.isAcceptable()) {
//获取ServerSocketChannel实例
ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
//接收用户的连接,该次获取结果不会阻塞
SocketChannel socketChannel = channel.accept();
System.out.println("客户端" + socketChannel.getRemoteAddress() + "连接上服务端");
//进行读写操作 -->以读操作为例
//将socketChannel通道设置为非阻塞
socketChannel.configureBlocking(false);
//将socketChannel通道注册到复用器中,并监听read事件
socketChannel.register(selector, SelectionKey.OP_READ);
}
//若有读事件完成
if (selectionKey.isReadable()) {
//获取channel实例,通过该通道来读取数据
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//进行读操作
ByteBuffer buffer = ByteBuffer.allocate(1024);
//将数据写入buffer中
int read = socketChannel.read(buffer);
if (read != -1) {
//读写模式切换 pos和lim指针的切换
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);//读数据
String msg = new String(bytes);
if (!"".equals(msg) && "bye".equals(msg)) {
//关闭socketChannel实例
socketChannel.close();
}
System.out.println("[recv]" + msg);
}
}
}
}
//关闭资源
selector.close();
serverSocketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 客户端编程
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
/**
* NIO客户端编程
*/
public class NIOClientDemo {
public static void main(String[] args) {
try {
//获取SocketChannel实例
SocketChannel socketChannel = SocketChannel.open();
//将socketChannel设置为非阻塞
socketChannel.configureBlocking(false);
//实例化复用器实例
Selector selector = Selector.open();
boolean isConnect = socketChannel.connect(new InetSocketAddress("127.0.0.1", 8889));
if(!isConnect){
//连接操作未完成,说明connect事件还未完成,将socketChannel注册到复用器,并关注connect事件
socketChannel.register(selector,SelectionKey.OP_CONNECT);
//等待复用器中事件的完成
selector.select();
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
iterator.remove();
if(selectionKey.isConnectable()){
SocketChannel channel = (SocketChannel)selectionKey.channel();
//可连接事件完成
channel.finishConnect();
}
}
}
//进行读事件,相当于给buffer中写数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("helloworld".getBytes());
//读写模式切换
buffer.flip();
//往socketChannel通道中写数据,即读取buffer中的数据
socketChannel.write(buffer);
//关闭资源
selector.close();
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.应用场景:
- BIO
适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,是JDK1.4之前的唯一选择, 但程序简单直观容易理解。
- NIO
适用于连接数目多且连接比较短(轻操作)的架构,比如聊推荐服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
参考博客:
https://blog.csdn.net/qq_41652863/article/details/90451133
https://www.cnblogs.com/javalyy/p/8882066.html
https://blog.csdn.net/dhrtyuhrthfgdhtgfh/article/details/90271123
来源:CSDN
作者:BUZZ_Q
链接:https://blog.csdn.net/BUZZ_Q/article/details/104143906