TCP协议、网络通信IO 整理记录

TCP协议:面向连接,可靠的传输协议。

位于传输控制层,用户建立连接进行可靠数据传输。

标识含义
ACK确认号是否有效,一般置为1
SYN请求建立连接,并在其序列号的字段进行序列号的初始值设定。建立连接,设置为1
FIN希望断开连接

三次握手(建立连接)

  1. 客户端请求建立连接,向服务端发送 SYN 包,等待服务端确认。
  2. 服务确认客户端的 SYN 包,并且发送自己的 SYN包(SYN+ACK),等待客户端确认。
  3. 客户端确认收到服务端的 SYN包,返回ACK。

tpc_1
tcp_cmd

双方确认建立连接之后(3次握手),会在各自的内存里,开辟一些资源(队列,空间,socket)
当双方都有资源为对发服务,就是有连接了,应用程序就可以读取连接里的资源buff(相当于读取连接)

为什么有3次握手?

  • 传输通信是双向的,双方都要确认彼此已经准备好
  • 如果不3次握手,服务器就会开辟资源一直等包

心跳,超时

  • 默认TCP会一直连接 ,而如果客户端蹦了,而服务不知道,就会一直启着占用资源。所有为有心跳检查,服务发送探测报文,一直都没有响应就会断开连接

Socket( 65535)
插座,套接字,(四元组,ip:port + ip:port ,双方建立唯一的连接),用于描述IP地址和端口,是一个通信链的句柄。
这个连接的一端称为一个socket

四次分手(双方销毁资源)

  • 1.客户端服务端发送 FIN(连接释放报文),并且停止发送数据,表示想分手断开连接
  • 2.服务端收到后,返回 ACK确认,表示收到
  • 3.服务端发送 FIN客户端,表示断开连接
  • 4.客户端收到,返回 ACK,确认收到;到此双方都确认分手断开连接
    tcp_4
    tcp_4

网络通信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();
        }
    }

}

1
22

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值