TCP协议:面向连接,可靠的传输协议。
位于传输控制层,用户建立连接进行可靠数据传输。
标识 | 含义 |
---|---|
ACK | 确认号是否有效,一般置为1 |
SYN | 请求建立连接,并在其序列号的字段进行序列号的初始值设定。建立连接,设置为1 |
FIN | 希望断开连接 |
三次握手(建立连接)
- 客户端请求建立连接,向服务端发送 SYN 包,等待服务端确认。
- 服务确认客户端的 SYN 包,并且发送自己的 SYN包(SYN+ACK),等待客户端确认。
- 客户端确认收到服务端的 SYN包,返回ACK。
双方确认建立连接之后(3次握手),会在各自的内存里,开辟一些资源(队列,空间,socket)
当双方都有资源为对发服务,就是有连接了,应用程序就可以读取连接里的资源buff(相当于读取连接)
为什么有3次握手?
- 传输通信是双向的,双方都要确认彼此已经准备好
- 如果不3次握手,服务器就会开辟资源一直等包
心跳,超时
- 默认TCP会一直连接 ,而如果客户端蹦了,而服务不知道,就会一直启着占用资源。所有为有心跳检查,服务发送探测报文,一直都没有响应就会断开连接
Socket( 65535)
插座,套接字,(四元组,ip:port + ip:port ,双方建立唯一的连接),用于描述IP地址和端口,是一个通信链的句柄。
这个连接的一端称为一个socket
四次分手(双方销毁资源)
- 1.客户端向服务端发送 FIN(连接释放报文),并且停止发送数据,表示想分手断开连接
- 2.服务端收到后,返回 ACK确认,表示收到
- 3.服务端发送 FIN给客户端,表示断开连接
- 4.客户端收到,返回 ACK,确认收到;到此双方都确认分手断开连接
网络通信I/O
- 内核: 是个程序,对外管理了所有的IO设备,属于中间层。
- 保护模式:用户程序不能直接调用内核程序。
- 如果调用底层?
- 中断,系统调用
程序在使用网络编程的时候,怎么调用,内核的系统调用 (软中断 int x80)?
- CPU正在执行用户程序时,程序想调用内核的一个方法,就会产生系统调用.
- 系统调用:
- 1.调用内核的一个方法,编译器编译的时候,会将指令 编译成软中断指令 INT X80,并植入程序当前调用的函数名称,放入寄存器。
- 2.当CPU读到 INT X80时,开始保护现场,CPU把程序的所有寄存器的值写回内存里;
- 3.然后开始调用内核,根据曾经传的参数,确定调用系统调用的哪个函数,然后再把值带回到啊程序,恢复现场。
弊端:系统调用,有进程切换的损耗.
IO 发展历程 ↓
- 普通 BIO 通信
public class TestSocket {
public static void main(String[] args) throws IOException {
//开启一个服务端,监听8090端口
ServerSocket server = new ServerSocket(8090);
System.out.println("服务启动监听 8090");
while (true){
//组塞等待连接,连接成功返回一个 Socket客户端
Socket client = server.accept();
System.out.println("客户端连入:"+client.getPort());
//开启一个线程读取数据
new Thread(new Runnable() {
Socket ss;
public Runnable setSS(Socket s){
ss=s;
return this;
}
@Override
public void run() {
try {
InputStream in = ss.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
//阻塞等待客户端发送数据
System.out.println("等待读取数据");
while (true){
System.out.println(reader.readLine());
}
}catch (IOException e){
e.printStackTrace();
}
}
}.setSS(client)
).start();
}
}
}
注意放在外层,不要有包结构。如 ↓
运行代码
strace -ff -o out /opt/jdk1.8.0_241/bin/java TestSocket
,启动并追踪线程
生成 6069 - 6078,共10个线程
内部系统调用 ↓
:set nu
,分行查询
java线程创建:
- 通过调用内核的系统调用,得到一个在操作系统里的一个轻量级的进程。
建立连接
nc localhost 8090
总结:
- 特点:每线程对应每连接
- 优势: 可以接受很多连接
- 弊端: 线程内存浪费,cpu调度消耗。 blocking 阻塞,accept 、rece会造成阻塞,所以避免阻塞时相互干预就会创建一个新的线程
随着 内核 的发展,出现了 NIO ,N : nonblocking ,单纯不阻塞demo 👇
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
//JDK 1.7之后
public class SocketNIO {
public static void main(String[] args) throws IOException, InterruptedException {
//一个线程解决
LinkedList<SocketChannel> clients = new LinkedList<>();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9090));
//false 设置不阻塞
serverSocketChannel.configureBlocking(false);
while (true){
Thread.sleep(1000);
// 不会阻塞 ; linux: -1 java: null
SocketChannel client = serverSocketChannel.accept();
if (client==null){
System.out.println("null..");
}else {
//有数据我就读,没有数据我就返 -1
client.configureBlocking(false);
int port = client.socket().getPort();
System.out.println("port:"+port);
clients.add(client);
}
//分配一个新的直接字节缓冲区
ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
for (SocketChannel c : clients) {
// 数据读取 >0 0 -1,不会阻塞
int num = c.read(buffer);
if (num>0){
//翻转, 将Buffer从写模式切换到读模式
buffer.flip();
byte[] aaa = new byte[buffer.limit()];
buffer.get(aaa);
String b = new String(aaa);
System.out.println(c.socket().getPort() + ":" + b);
buffer.clear();
}
}
}
}
}
总结:
- 优势:规避了多线程的问题
- 弊端:可能造成无意义的系统调用(recv),
for { recv(client,fd) }
,基于每一个客户端都会调用recv
尝试读取,有1w个就会尝试读取1w次,而如果就有1次有数据就会有9999次是无意义的。- 解决:一次调用把1w个客户端传给内核,由内核遍历,然后返回有几个是有数据的客户端,然后再在用户空间,调用去读有返回数据的。
- 复用:这里就复用了这一次系统调用,引出 多路复用器 ↓
多路复用器
- synchronous I/O multiplexing
(同步)
- select (1024),poll (遵从操作系统)
- epoll
select 、poll | epoll
描述:
man 2 select
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
DESCRIPTION
select() and pselect() allow a program to monitor multiple file descriptors, waiting until one or more of the file descriptors become "ready" for some class
of I/O operation (e.g., input possible). A file descriptor is considered ready if it is possible to perform a corresponding I/O operation (e.g., read(2)
without blocking, or a sufficiently small write(2)).
//翻译
/*select()和 pselect() 允许一个程序监控多个文件描述符,直到一个或多个文件描述符为某些类“准备好”
I/O操作(例如,输入可能)。如果可以执行相应的I/O操作(例如,read(2)),则认为文件描述符已经准备就绪。
没有阻塞,或足够小的写(2))。
*/
大体表现:
while(true){
select(fds) // 放入所有客户端,内核返回哪些可读 O(1)
recv(fd) // 用户再具体调用可读的
}
注*:是否可以通过多路复用器,达到快速读取IO的目的?
- 否 !
- 内核只提供了,哪些文件描述符fd ,可读可写;程序只是得到了状态,需要程序去调用和 recv()
总结:
- 优势:通过一次系统调用,把fds,传递给内核,内核内部进行遍历,这种遍历减少了系统调用的次数。
- 弊端:重复传递fds,每次select、poll都要重新遍历。
- 解决:内核开辟空间,保留fd。
NIO ,多路复用器,简单演示demo
- 简述:ServerSocketChannel 绑定端口设置非阻塞,打开selector选择器注册到ServerSocketChannel 中,设置监听 SelectionKey.OP_ACCEPT(连接事件),死循环监听每一个Channel通道的事件(Selector.select())放入集合中,遍历集合
Set<SelectionKey>
,处理事件后移除,而数据的处理通过缓冲区进行.
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
/**
* @program: he
* @description: 多路复用,服务端
* 单线程,单个selector demo
* @author: he
* @create: 2020-10-23 15:46
**/
public class SocketMultipleServer {
private ServerSocketChannel server=null;
private Selector selector = null;
/**
* 可以 多 selector, 开多线程,每个selector 负责不同的功能
*/
int port=9090;
private int BLOCK = 8192;
/**
* 接收数据缓冲区
*/
private final ByteBuffer sendBuffer = ByteBuffer.allocate(BLOCK);
/**
* 发送数据缓冲区
*/
private final ByteBuffer receiveBuffer = ByteBuffer.allocate(BLOCK);
public static void main(String[] args) {
new Thread(()-> new SocketMultipleServer().start()).start();
}
public void initServer(){
try {
server=ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));
//可能是 select poll epoll , 优选选择 epoll,如果是 epoll , open() 》 epoll_create -> fd3 约等于在内核开辟了一个空间
//select poll,在java进程里面开辟了一个空间
selector=Selector.open();
//如果是select poll在jvm开辟一个数组 fd4 放进去
//epoll 传入fd到内核
server.register(selector, SelectionKey.OP_ACCEPT);
}catch (Exception e){
e.printStackTrace();
}
}
public void start(){
initServer();
System.out.println("start..");
try {
while (true){
Set<SelectionKey> keys = selector.keys();
System.out.println(keys.size()+ " size ");
//查询有哪些注册过的可以读写的IO
//如果是epoll 相当于 epoll_wait()
while (selector.select(500)>0){
System.out.println("开始处理有数据的IO..");
//返回有状态的fd集合
Set<SelectionKey> selectionKeys = selector.keys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
//遍历IO
while (iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()){
//正在监听的
acceptHandler(key);
}else if (key.isReadable()){
//建立连接的
readHandler(key);
}else if (key.isWritable()) {
writeHandler(key);
}
}
}
}
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 接收
* @param key
*/
public void acceptHandler(SelectionKey key){
try {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//接收客户端
SocketChannel client = ssc.accept();
client.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(8192);
client.register(selector,SelectionKey.OP_ACCEPT,byteBuffer);
System.out.println("---新客户端:"+client.getRemoteAddress());
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 读取
* @param key
*/
public void readHandler(SelectionKey key){
int count;
String receiveText;
try {
SocketChannel channel = (SocketChannel)key.channel();
receiveBuffer.clear();
count = channel.read(receiveBuffer);
if (count > 0) {
receiveText = new String( receiveBuffer.array(),0,count);
System.out.println("服务器端接受客户端数据--:"+receiveText);
channel.register(selector, SelectionKey.OP_WRITE);
}
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 写入
* @param key
*/
public void writeHandler(SelectionKey key){
//标识数字
int flag = 0;
try {
sendBuffer.clear();
// 返回为之创建此键的通道。
SocketChannel channel = (SocketChannel) key.channel();
flag++;
String sendText="message from server--" + flag;
//向缓冲区中输入数据
sendBuffer.put(sendText.getBytes());
//将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
sendBuffer.flip();
//输出到通道
channel.write(sendBuffer);
System.out.println("服务器端向客户端发送数据--:"+sendText);
channel.register(selector, SelectionKey.OP_READ);
}catch (Exception e){
e.printStackTrace();
}
}
}