代码连接:
一、网络通讯基础
1、OSI七层模型
2、一个域名底层是如何解析
浏览器访问域名,根据域名先从本地host文件C:\Windows\System32\drivers\etc\hosts文件 查找匹配对应的ip与域名,如果本地Host文件 没有的情况下,则联网去电信运营商查找。
3、什么是Socket技术?
Socket就是套接字,是两个程序之间通过双向信道进行数据交换的端,可以理解为接口。使用Socket编程也称为网络编程,Socket只是接口并不是网络通信协议。
TCP以及UDP协议实现了Socket技术,它们都是在OSI七层网络模型的传输层的。
4、什么是TCP
TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议,建立通讯前TCP需要经过三次握手,断开连接前需要经过四次挥手。
TCP协议应用场景:HTTP、HTTPS、FTP协议
5、什么是UDP
UDP是面向无连接通讯协议,udp通讯时不需要接受方确定,属于不可靠传输,可能会存在丢包的现象。
UDP协议应用场景:QQ语音、QQ视频
6、三次挥手概念理解
tcp建立连接中,有一些名词表示需要解释:
syn就是建立连接、ack就是确认标志、fin终止标志
图解:
第一次握手:客户端会向服务器端发送码为syn=1,随机产生一个seq_number=x的数据包到服务器端 (syn)
第二次握手:服务端接受到客户端请求之后,确认ack=x+1, 于是就向客户端发送syn(服务端独立生成 随机生成数字Y)+ack
第三次握手:客户端接受syn(随机数Y)+ack,向服务器端发送ack=y+1,此包发送完毕即可 建立tcp连接。
翻译:
第一次握手:客户端询问服务器端是否启动
第二次握手:服务器端监听到客户端信息 ,向客户端回话,服务器端已启动,可以建立连接
第三次握手:客户端收到服务器端已启动的信息,开始建立连接
三次握手发生时机:
客户端和服务器端需要建立TCP连接的时候,这个时候要确保服务服务器端是先启动的,然后再启动客户端。
7、四次挥手概念理解
流程图:
第一次挥手: 客户端向服务器端发送释放的报文,停止发送数据 fin=1、生成一个序列号seq=u;
第二次挥手: 服务器端接受到释放的报文后,发送ack=u+1;随机生成的seq=v给客户端;当前状态为关闭等待状态
客户端收到了服务器确认通知之后,此时客户端就会进入到终止状态,等待服务器端发送释放报文。
第三次挥手:服务器端最后数据发送完毕之后,就向客户端发送连接释放报文,FIN=1,ack=u+1 当前为半关闭状态,随机生成一个随机树w
第四次挥手,客户端必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
8、TCP代码
package com.daxiong.tcp; import java.io.IOException; import java.io.PrintWriter; import java.net.*; public class SocketTcpServer { public static void main(String[] args) throws IOException { // 创建Server Socket ServerSocket serverSocket = new ServerSocket(); // 创建我们的 Socket 监听连接地址和端口号 SocketAddress address = new InetSocketAddress(InetAddress.getLocalHost(), 8080); // 绑定我们的监听地址 serverSocket.bind(address); // 等待接受请求 System.out.println("等待客户端发送消息.."); Socket accept = serverSocket.accept(); // 获取OutputStream流 PrintWriter socketOut = new PrintWriter(accept.getOutputStream()); byte buf[] = new byte[1024]; if (accept.getInputStream().read(buf) > 0) { System.out.println("服务器端接受到客户端消息:" + new String(buf)); } // 服务器端响应消息 String sendStr = "自己煮面"; socketOut.write(sendStr); socketOut.flush(); // 关闭所有连接 socketOut.close(); accept.close(); serverSocket.close(); } }
package com.daxiong.tcp; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; public class SocketTcpClient { public static void main(String[] args) throws IOException { // 创建socket final Socket socket = new Socket(); // 创建socket地址 SocketAddress address = new InetSocketAddress(InetAddress.getLocalHost(), 8080); socket.connect(address); // 创建PrintWriter PrintWriter socketOut = new PrintWriter(socket.getOutputStream()); BufferedReader socketIn = new BufferedReader( new InputStreamReader(socket.getInputStream())); // 向服务器发送的内容 String sendStr = "客户端问服务器端:今天早晨吃什么?"; socketOut.write(sendStr); socketOut.flush(); String receiveStr = socketIn.readLine(); System.out.println("服务器端回复:: " + receiveStr); // 关闭连接 socketOut.close(); socketIn.close(); socket.close(); } }
解释:TCP代码会发生两次阻塞
一次是服务器端在接受到客户端连接的时候会阻塞进行等待(serverSocket.accept(),服务端与客户端进行三次握手) ,另一次是服务器端在接收到到客户端发送到的信息的时候(accept.getInputStream().read(buf),服务端读取客户端发送过来的数据,涉及到IO模型)
在debug模式,调试一下就可以看到上述结果
9、udp代码
package com.daxiong.udp; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; public class SocketUdpServer { public static void main(String[] args) throws IOException { /* * 接收客户端发送的数据 */ //1.创建服务器端DatagramSocket,指定端口 DatagramSocket socket = new DatagramSocket(8800); //2.创建数据报,用于接收客户端发送的数据 byte[] data = new byte[1024]; //创建字节数组,指定接收的数据包的大小 DatagramPacket packet = new DatagramPacket(data, data.length); //3.接收客户端发送的数据 System.out.println("****服务器端已经启动,等待客户端发送数据"); //此方法在接收到数据报之前会一直阻塞 socket.receive(packet); //4.读取数据 String info = new String(data, 0, packet.getLength()); System.out.println("我是服务器,客户端说:" + info); /* * 向客户端响应数据 */ //1.定义客户端的地址、端口号、数据 InetAddress address = packet.getAddress(); int port = packet.getPort(); byte[] data2 = "自己煮面~~".getBytes(); //2.创建数据报,包含响应的数据信息 DatagramPacket packet2 = new DatagramPacket(data2, data2.length, address, port); //3.响应客户端 socket.send(packet2); //4.关闭资源 socket.close(); } }
package com.daxiong.udp; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; public class SocketUdpClient { public static void main(String[] args) throws Exception { /* * 向服务器端发送数据 */ //1.定义服务器的地址、端口号、数据 InetAddress address = InetAddress.getByName("localhost"); int port = 8800; byte[] data = "早餐吃啥?".getBytes(); //2.创建数据报,包含发送的数据信息 DatagramPacket packet = new DatagramPacket(data, data.length, address, port); //3.创建DatagramSocket对象 DatagramSocket socket = new DatagramSocket(); //4.向服务器端发送数据报 socket.send(packet); /* * 接收服务器端响应的数据 */ //1.创建数据报,用于接收服务器端响应的数据 byte[] data2 = new byte[1024]; DatagramPacket packet2 = new DatagramPacket(data2, data2.length); //2.接收服务器响应的数据 socket.receive(packet2); //3.读取数据 String reply = new String(data2, 0, packet2.getLength()); System.out.println("我是客户端,服务器说:" + reply); //4.关闭资源 socket.close(); } }
二、HTTP协议与IO模型
1、HTTP协议
Http协议一种超文本传输的协议,基于TCP/IP协议的包装
2、HTTP的特征
- 无状态
- 请求与响应模型
- 简单快速
- 灵活可以传输任何类型
3、请求(待补充)
4、响应 (待补充)
5、同步和异步的区别
同步也就是程序从上往下实现执行;
异步重新开启一个新分支,执行后续的代码,主线程直接返回结果;
从Http协议的角度分析同步与异步区别:
我们的Http协议请求默认情况下同步形式调用,如果调用过程非常耗时的情况下 客户端等待时间就非常长, 这种形式我们可以理解阻塞式;
解决办法:耗时的代码我们可以使用多线程或者MQ实现处理,但是不能立马获取结果; 客户端可以主动查询
多线程以及MQ实现多线程的图解:
6、用户态和内核态的区别
内核态: CPU可以访问内存所有数据, 包括硬件设备, 例如硬盘, 网卡;
用户态: (独立创建应用程序) 只能受限的访问内存, 且不允许访问硬件设备. 占用CPU的能力被剥夺, CPU资源可以被其他程序获
7、阻塞与非阻塞的区别
阻塞:如果当前线程没有获取到结果的情况下,当前线程从运行状态切换为阻塞状态
从内核角度分析:当前线程从用户空间切换到内核空间
非阻塞:如果当前线程没有获取到结果的情况下,当前的线程不会阻塞,当前线程一直运行在用户空间
8、IO模型 (用于在建立三次握手连接之后,读取数据这个时候)
现在的IO模型分为三:
同步阻塞模型:BIO
同步非阻塞模型:NIO
异步费阻塞模型:AIO
(1)BIO(Blocking IO) 同步阻塞模型
一个线程处理一个客户端请求;
缺点:
IO代码里read操作是阻塞操作,如果获取不到数据的情况下,那么服务端就会陷入阻塞,不能再接收其他客户端的链接,为了解决这个问题,服务端与客户端在建立三次握手之后,在读取数据的操作是上使用多线程处理,一个客户端连接到来的时候就会用一个线程去处理这个客户端请求,在客户端请求较多的时候,就会导致大量的线程被创建,非常消耗CPU的资源。
应用场景:
BIO 方式适用于连接数目比较小且固定的架构, 这种方式对服务器资源要求比较高
代码演示:
第一种情况:bio模型正常演示服务端与消费端通信
package com.daxiong.netty.bio1;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* bio模型正常演示服务端与消费端通信
* SocketBioServer
**/
public class SocketBioServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9001);
while (true) {
System.out.println("服务器端正在等待连接中...");
// 阻塞方法 如果没有客户端与服务器端建立连接时,该方法会阻塞等待
final Socket socket = serverSocket.accept();
System.out.println("有客户端和我连接啦");
// 客户端建立TCP连接后,客户端有发消息
handler(socket);
}
}
private static void handler(Socket socket) throws IOException {
System.out.println("线程id= " + Thread.currentThread().getId());
byte[] bytes = new byte[1024];
System.out.println("开始read。。");
//接收客户端的数据,如果没有读取到客户端数据时,该方法也会阻塞
int read = socket.getInputStream().read(bytes);
System.out.println("read结束");
if (read != -1) {
System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
System.out.println("线程id= = " + Thread.currentThread().getId());
}
socket.getOutputStream().write("效果演示完毕~~".getBytes());
socket.getOutputStream().flush();
}
}
package com.daxiong.netty.bio1;
import java.io.IOException;
import java.net.Socket;
/***
* SocketBioClient
* bio模型正常演示服务端与消费端通信
*/
public class SocketBioClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 9001);
//向服务端发送数据
socket.getOutputStream().write("来演示下同步阻塞Bio".getBytes());
socket.getOutputStream().flush();
System.out.println("向服务端发送数据结束");
byte[] bytes = new byte[1024];
//接收服务端回传的数据
socket.getInputStream().read(bytes);
System.out.println("接收到服务端的数据:" + new String(bytes));
socket.close();
}
}
第二种情况:bio模型演示服务端接收消费端发送一个空消息,这样服务端就会一直阻塞在这个消费端,不能再处理其他消费端的连接
package com.daxiong.netty.bio2; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; /** * bio模型演示服务端接收消费端发送一个空消息,这样服务端就会一直阻塞在这个消费端,不能再处理其他消费端的连接 * SocketBioServer **/ public class SocketBioServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(9001); while (true) { System.out.println("服务器端正在等待连接中..."); // 阻塞方法 如果没有客户端与服务器端建立连接时,该方法会阻塞等待 final Socket socket = serverSocket.accept(); System.out.println("有客户端和我连接啦"); // 客户端建立TCP连接后,客户端有发消息 handler(socket); } } private static void handler(Socket socket) throws IOException { System.out.println("线程id= " + Thread.currentThread().getId()); byte[] bytes = new byte[1024]; System.out.println("开始read。。"); //接收客户端的数据,如果没有读取到客户端数据时,该方法也会阻塞 int read = socket.getInputStream().read(bytes); System.out.println("read结束"); if (read != -1) { System.out.println("接收到客户端的数据:" + new String(bytes, 0, read)); System.out.println("线程id= = " + Thread.currentThread().getId()); } socket.getOutputStream().write("效果演示完毕~~".getBytes()); socket.getOutputStream().flush(); } }
package com.daxiong.netty.bio2; import java.io.IOException; import java.net.Socket; /*** * SocketBioClient * bio模型演示服务端接收消费端发送一个空消息,这样服务端就会一直阻塞在这个消费端,不能再处理其他消费端的连接 */ public class SocketBioClient { public static void main(String[] args) throws IOException { Socket socket = new Socket("127.0.0.1", 9001); //注释下面 ,向服务端发送一条空消息数据 // socket.getOutputStream().write("来演示下同步阻塞Bio".getBytes()); socket.getOutputStream().flush(); System.out.println("向服务端发送数据结束"); byte[] bytes = new byte[1024]; //接收服务端回传的数据 socket.getInputStream().read(bytes); System.out.println("接收到服务端的数据:" + new String(bytes)); socket.close(); } }
当有其他客户端连接这个服务器端的时候就会报异常。
第三种情况:bio模型 使用多线程的方式解决有消费者发送空消息而导致服务端不能连接处理其他消费者的请求
package com.daxiong.netty.bio3; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; /** * bio模型 使用多线程的方式解决有消费者发送空消息而导致服务端不能连接处理其他消费者的请求 * SocketBioServer **/ public class SocketBioServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(9001); while (true) { System.out.println("服务器端正在等待连接中..."); // 阻塞方法 如果没有客户端与服务器端建立连接时,该方法会阻塞等待 final Socket socket = serverSocket.accept(); System.out.println("有客户端和我连接啦"); // 客户端建立TCP连接后,处理客户端消息 //使用多线程,当有一个客户端要来建立连接的时候,就创建一个线程来处理客户端的连接请求,这样能防止一个客户端发送过来一个空消息 // 导致服务端一直阻塞等待导致服务端不能处理其他客户端的请求,这样做的缺点就是非常消费CPU的资源 new Thread(new Runnable() { public void run() { try { handler(socket); }catch (IOException e){ } } }); } } private static void handler(Socket socket) throws IOException { System.out.println("线程id= " + Thread.currentThread().getId()); byte[] bytes = new byte[1024]; System.out.println("开始read。。"); //接收客户端的数据,如果没有读取到客户端数据时,该方法也会阻塞 int read = socket.getInputStream().read(bytes); System.out.println("read结束"); if (read != -1) { System.out.println("接收到客户端的数据:" + new String(bytes, 0, read)); System.out.println("线程id= = " + Thread.currentThread().getId()); } socket.getOutputStream().write("效果演示完毕~~".getBytes()); socket.getOutputStream().flush(); } }
package com.daxiong.netty.bio3; import java.io.IOException; import java.net.Socket; /*** * SocketBioClient * bio模型 使用多线程的方式解决有消费者发送空消息而导致服务端不能连接处理其他消费者的请求 */ public class SocketBioClient { public static void main(String[] args) throws IOException { Socket socket = new Socket("127.0.0.1", 9001); //向服务端发送数据 socket.getOutputStream().write("来演示下同步阻塞Bio".getBytes()); socket.getOutputStream().flush(); System.out.println("向服务端发送数据结束"); byte[] bytes = new byte[1024]; //接收服务端回传的数据 socket.getInputStream().read(bytes); System.out.println("接收到服务端的数据:" + new String(bytes)); socket.close(); } }
(2)NIO 同步非阻塞
NIO同步非阻塞的原理:多个客户端发送连接请求注册到(多路复用器)selector中,
多路复用器使用轮训机制实现检测每个io请求有数据就进行处理。
底层实现原理:
I/O多路复用底层一般用的Linux API(select,poll,epoll)来实现
NIO 有三大核心组件: Channel(通道), Buffer(缓冲区),Selector(选择器)
1.Channel(通道) :称之为通道,和IO相连,通信双方进行数据交流的通道,需要和buffer结合使用。
2.Buffer(缓冲区) :对数据的读取/写入需要使用buffer,buffer本质就是一个数组。
3.Selector(选择器): IO多路复用 一个线程Thread使用选择器Selector通过轮询的方式去监听多个通道Channel上的事件,从而让一个线程可以处理多个事件。
代码:
package com.daxiong.netty.nio; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; /** * 模拟伪装nio底层原理 * SocketNioTcpServer */ public class SimulationNioTcpServer { /** * 保存SocketChannel */ private static List<SocketChannel> listSocketChannel = new ArrayList<SocketChannel>(); /** * 缓冲区大小 */ private static ByteBuffer byteBuffer = ByteBuffer.allocate(512); public static void main(String[] args) throws Exception { try { // 1.创建一个ServerSocketChannel ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 2. 绑定地址 InetSocketAddress socketAddress = new InetSocketAddress(8080); ServerSocketChannel bind = serverSocketChannel.bind(socketAddress); // 3.设置非阻塞模式,三次握手的时候不阻塞 serverSocketChannel.configureBlocking(false); while (true) { // 4.等待建立连接 如果设置非阻塞的情况下,如果没有获取连接的情况下直接返回null,如果建立连接之后返回socketChannel // 建立三次握手 SocketChannel socketChannel = serverSocketChannel.accept(); // 5. 如果socketChannel 不为空的情况下,则将该连接保存起来。 if (socketChannel != null) { // 设置该socketChannel通道为fasle,未接收到客户端消息的时候,不阻塞 socketChannel.configureBlocking(false); listSocketChannel.add(socketChannel); } // 循环SocketChannel,检查每个SocketChannel中数据有传输数据 for (SocketChannel scl : listSocketChannel) { try { // 6.以缓冲区方式读取 int read = scl.read(byteBuffer); if (read > 0) { byteBuffer.flip(); // 转换格式为中文的格式 Charset charset = Charset.forName("UTF-8"); String receiveText = charset.newDecoder().decode (byteBuffer.asReadOnlyBuffer()).toString(); System.out.println("receiveText:" + receiveText); } } catch (Exception e) { e.printStackTrace(); } } } } catch (Exception e) { e.printStackTrace(); } } } |
网络调试工具:在码云仓库中有
NIO同步非阻塞的底层原理
IO多路复用 一个线程Thread使用选择器Selector通过轮询的方式去监听多个通道Channel上的事件,从而让一个线程可以处理多个事件。
I/O多路复用底层一般用的Linux API(select,poll,epoll)来实现
总结:
1.使用select的情况下,底层采用该数组方式存放 每次调用遍历的时间复杂度就是为O(n),有可能会产生空轮训,比如 保存1万个连接,最终只有1个连接有传输数据。
2.使用poll底层采用链表结构存放,每次调用遍历的时间复杂度就是为O(n)
Poll与select之间区别不是很大;select监视器单个进程可监视的fd数量被限制
可以通过cat /proc/sys/fs/file-max, poll 是没有监视的fd数量限制。
Linux服务器中创建Socket服务器端 单个select进程可监事的fd(连接数据)限制
3.
epoll采用事件通知回调方式,避免空轮休时间复杂度为o(1);