博客记录-day065-LinkedList源码解析+TCP面试题

63 阅读21分钟

一、Java全栈知识体系-LinkedList源码解析

1、概述

LinkedList同时实现了List接口和Deque接口,也就是说它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(stack)。这样看来,LinkedList简直就是个全能冠军。当你需要使用栈或者队列时,可以考虑使用LinkedList,一方面是因为Java官方已经声明不建议使用Stack类,更遗憾的是,Java里根本没有一个叫做Queue的类(它是个接口名字)。关于栈或队列,现在的首选是ArrayDeque,它有着比LinkedList(当作栈或队列使用时)有着更好的性能。

LinkedList_base

LinkedList的实现方式决定了所有跟下标相关的操作都是线性时间,而在首段或者末尾删除元素只需要常数时间。为追求效率LinkedList没有实现同步(synchronized),如果需要多个线程并发访问,可以先采用Collections.synchronizedList()方法对其进行包装。

2、LinkedList实现

2.1 底层数据结构

LinkedList底层通过双向链表实现。双向链表的每个节点用内部类Node表示。LinkedList通过firstlast引用分别指向链表的第一个和最后一个元素。注意这里没有所谓的dummy,当链表为空的时候firstlast都指向null

// 表示链表中的元素数量
transient int size = 0;

/**
 * 指向链表第一个节点的指针。
 * 不变性:要么(first == null && last == null),
 *        要么(first.prev == null && first.item != null)
 */
transient Node<E> first;

/**
 * 指向链表最后一个节点的指针。
 * 不变性:要么(first == null && last == null),
 *        要么(last.next == null && last.item != null)
 */
transient Node<E> last;

其中Node是私有的内部类:

    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

2.2 构造函数

   /**
 * 构造一个空的链表。
 */
public LinkedList() {
}

/**
 * 构造一个包含指定集合元素的链表,这些元素按照集合迭代器返回的顺序排列。
 *
 * @param  c 要放入这个链表的元素的集合
 * @throws NullPointerException 如果指定的集合为 null
 */
public LinkedList(Collection<? extends E> c) {
    this(); // 调用无参构造函数,初始化一个空链表
    addAll(c); // 将指定集合中的所有元素添加到链表中
}

2.3 getFirst(), getLast()

获取第一个元素, 和获取最后一个元素:

/**
 * 返回链表中的第一个元素。
 *
 * @return 链表中的第一个元素
 * @throws NoSuchElementException 如果链表为空
 */
public E getFirst() {
    final Node<E> f = first; // 获取链表的第一个节点
    if (f == null) // 如果链表为空
        throw new NoSuchElementException(); // 抛出异常
    return f.item; // 返回第一个节点的元素
}

/**
 * 返回链表中的最后一个元素。
 *
 * @return 链表中的最后一个元素
 * @throws NoSuchElementException 如果链表为空
 */
public E getLast() {
    final Node<E> l = last; // 获取链表的最后一个节点
    if (l == null) // 如果链表为空
        throw new NoSuchElementException(); // 抛出异常
    return l.item; // 返回最后一个节点的元素
}

2.4 removeFirst(), removeLast(), remove(e), remove(index)

remove()方法也有两个版本,一个是删除跟指定元素相等的第一个元素remove(Object o),另一个是删除指定下标处的元素remove(int index)

LinkedList_remove.png

删除元素 - 指的是java, 如果没有这个元素,则返回false;判断的依据是equals方法, 如果equals,则直接unlink这个node;由于LinkedList可存放null元素,故也可以删除第一次出现null的元素

/**
 * 从链表中删除指定元素的第一个出现,如果存在的话。如果链表中不包含该元素,则保持不变。
 * 更正式地说,删除具有最低索引的元素。
 * 如果链表包含指定元素(或者等效地,如果链表由于调用而发生了变化),则返回 true。
 *
 * @param o 要从链表中删除的元素,如果存在
 * @return 如果链表包含指定元素,则返回 {@code true}
 */
public boolean remove(Object o) {
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

/**
 * 删除非空节点 x。
 */
E unlink(Node<E> x) {
    // 断言 x != null;
    final E element = x.item; // 获取节点x的元素
    final Node<E> next = x.next; // 获取节点x的后继节点
    final Node<E> prev = x.prev; // 获取节点x的前驱节点

    if (prev == null) { // 如果x是第一个元素
        first = next;
    } else {
        prev.next = next; // 将x的前驱节点的后继设置为x的后继
        x.prev = null; // 将x的前驱设置为null,帮助垃圾回收
    }

    if (next == null) { // 如果x是最后一个元素
        last = prev;
    } else {
        next.prev = prev; // 将x的后继节点的前驱设置为x的前驱
        x.next = null; // 将x的后继设置为null,帮助垃圾回收
    }

    x.item = null; // 将x的元素设置为null,帮助垃圾回收
    size--; // 链表大小减1
    modCount++; // 修改次数加1
    return element; // 返回被删除的元素
}

remove(int index)使用的是下标计数,只需要判断该index是否有元素即可,如果有则直接unlink这个node。

    /**
     * Removes the element at the specified position in this list.  Shifts any
     * subsequent elements to the left (subtracts one from their indices).
     * Returns the element that was removed from the list.
     *
     * @param index the index of the element to be removed
     * @return the element previously at the specified position
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }

删除head元素:

 /**
 * 移除并返回此列表的第一个元素。
 *
 * @return 此列表的第一个元素
 * @throws NoSuchElementException 如果此列表为空
 */
public E removeFirst() {
    final Node<E> f = first; // 获取第一个节点
    if (f == null)
        throw new NoSuchElementException(); // 如果链表为空,抛出异常
    return unlinkFirst(f); // 移除并返回第一个节点
}

/**
 * 解除非空第一个节点 f 的链接。
 */
private E unlinkFirst(Node<E> f) {
    // 断言 f == first && f != null;
    final E element = f.item; // 获取第一个节点的元素
    final Node<E> next = f.next; // 获取第一个节点的后继节点

    f.item = null; // 将第一个节点的元素设置为null,帮助垃圾回收
    f.next = null; // 将第一个节点的后继设置为null,帮助垃圾回收
    first = next; // 将链表的第一个节点设置为原第一个节点的后继

    if (next == null)
        last = null; // 如果原第一个节点没有后继,则链表现在为空
    else
        next.prev = null; // 如果有后继,将其前驱设置为null

    size--; // 链表大小减1
    modCount++; // 修改次数加1
    return element; // 返回被移除的元素
}

删除last元素:

/**
 * 移除并返回此列表的最后一个元素。
 *
 * @return 此列表的最后一个元素
 * @throws NoSuchElementException 如果此列表为空
 */
public E removeLast() {
    // 获取最后一个节点
    final Node<E> l = last;
    // 如果列表为空,抛出异常
    if (l == null)
        throw new NoSuchElementException();
    // 移除最后一个节点并返回其元素
    return unlinkLast(l);
}
/**
 * 断开非空的最后一个节点l的链接。
 */
private E unlinkLast(Node<E> l) {
    // 断言:l是最后一个节点且不为null
    // assert l == last && l != null;
    // 获取最后一个节点的元素
    final E element = l.item;
    // 获取最后一个节点的前一个节点
    final Node<E> prev = l.prev;
    // 清空最后一个节点的元素和前指针,帮助GC
    l.item = null;
    l.prev = null;
    // 更新last指针
    last = prev;
    // 如果前一个节点为null,说明列表变为空,更新first指针
    if (prev == null)
        first = null;
    else
        // 否则,更新前一个节点的后指针为null
        prev.next = null;
    // 列表大小减1
    size--;
    // 修改次数加1
    modCount++;
    // 返回最后一个节点的元素
    return element;
}

2.5 add()

add()方法有两个版本,一个是add(E e),该方法在LinkedList的末尾插入元素,因为有last指向链表末尾,在末尾插入元素的花费是常数时间。只需要简单修改几个相关引用即可;另一个是add(int index, E element),该方法是在指定下表处插入元素,需要先通过线性查找找到具体位置,然后修改相关引用完成插入操作。

    /**
     * Appends the specified element to the end of this list.
     *
     * <p>This method is equivalent to {@link #addLast}.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    
    /**
     * Links e as last element.
     */
    void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

LinkedList_add

add(int index, E element), 当index==size时,等同于add(E e); 如果不是,则分两步: 1.先根据index找到要插入的位置,即node(index)方法;2.修改引用,完成插入操作。

    /**
     * Inserts the specified element at the specified position in this list.
     * Shifts the element currently at that position (if any) and any
     * subsequent elements to the right (adds one to their indices).
     *
     * @param index index at which the specified element is to be inserted
     * @param element element to be inserted
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

上面代码中的node(int index)函数有一点小小的trick,因为链表双向的,可以从开始往后找,也可以从结尾往前找,具体朝那个方向找取决于条件index < (size >> 1),也即是index是靠近前端还是后端。从这里也可以看出,linkedList通过index检索元素的效率没有arrayList高。

    /**
     * Returns the (non-null) Node at the specified element index.
     */
    Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

2.6 addAll()

addAll(index, c) 实现方式并不是直接调用add(index,e)来实现,主要是因为效率的问题,另一个是fail-fast中modCount只会增加1次

   /**
 * 将指定集合中的所有元素追加到这个列表的末尾,其顺序由指定集合的迭代器返回。
 * 如果在操作过程中修改了指定的集合,则此操作的行为是未定义的。
 * (注意,如果指定的集合是这个列表且非空,则会出现这种情况。)
 *
 * @param c 包含要添加到此列表中的元素的集合
 * @return 如果此列表因调用而更改,则返回{@code true}
 * @throws NullPointerException 如果指定的集合为空
 */
public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}
/**
 * 将指定集合中的所有元素插入到此列表中,从指定位置开始。
 * 将当前位于该位置(如果有的话)的元素及其后的所有元素向右移动(增加它们的索引)。
 * 新元素将按照指定集合的迭代器返回的顺序出现在列表中。
 *
 * @param index 要插入指定集合中第一个元素的索引
 * @param c 包含要添加到此列表中的元素的集合
 * @return 如果此列表因调用而更改,则返回{@code true}
 * @throws IndexOutOfBoundsException {@inheritDoc}
 * @throws NullPointerException 如果指定的集合为空
 */
public boolean addAll(int index, Collection<? extends E> c) {
    checkPositionIndex(index);
    Object[] a = c.toArray();
    int numNew = a.length;
    if (numNew == 0)
        return false;
    Node<E> pred, succ;
    if (index == size) {
        succ = null;
        pred = last;
    } else {
        succ = node(index);
        pred = succ.prev;
    }
    // 遍历要添加的元素数组
    for (Object o : a) {
        @SuppressWarnings("unchecked") E e = (E) o;
        Node<E> newNode = new Node<>(pred, e, null);
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        pred = newNode;
    }
    // 如果插入位置在末尾,更新last指针
    if (succ == null) {
        last = pred;
    } else {
        // 否则,将新节点与后续节点连接起来
        pred.next = succ;
        succ.prev = pred;
    }
    // 更新列表大小和修改次数
    size += numNew;
    modCount++;
    return true;
}

2.7 clear()

为了让GC更快可以回收放置的元素,需要将node之间的引用关系赋空。

/**
 * 从此列表中移除所有元素。
 * 调用此方法后,列表将变为空。
 */
public void clear() {
    // 清除节点之间的所有链接是“不必要的”,但是:
    // - 如果被丢弃的节点占据不止一代,则有助于世代GC
    // - 即使存在可到达的迭代器,也能确保释放内存
    for (Node<E> x = first; x != null; ) {
        Node<E> next = x.next; // 保存下一个节点
        x.item = null; // 清空当前节点的元素
        x.next = null; // 断开当前节点对下一个节点的引用
        x.prev = null; // 断开当前节点对前一个节点的引用
        x = next; // 移动到下一个节点
    }
    first = last = null; // 重置首尾节点为null
    size = 0; // 重置列表大小为0
    modCount++; // 增加修改次数
}

2.8 Positional Access 方法

通过index获取元素

    /**
     * Returns the element at the specified position in this list.
     *
     * @param index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

将某个位置的元素重新赋值:

    /**
     * Replaces the element at the specified position in this list with the
     * specified element.
     *
     * @param index index of the element to replace
     * @param element element to be stored at the specified position
     * @return the element previously at the specified position
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E set(int index, E element) {
        checkElementIndex(index);
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }

将元素插入到指定index位置:

/**
 * 在列表的指定位置插入指定的元素。
 * 将当前位于该位置的元素(如果有的话)以及任何后续元素向右移动(即它们的索引加一)。
 *
 * @param index 指定元素要插入的位置的索引
 * @param element 要插入的元素
 * @throws IndexOutOfBoundsException 如果索引超出范围,则抛出此异常
 */
public void add(int index, E element) {
    // 检查索引位置是否合法
    checkPositionIndex(index);

    // 如果索引等于列表大小,则直接在末尾添加元素
    if (index == size)
        linkLast(element);
    else
        // 否则在指定位置的前一个节点后插入新元素
        linkBefore(element, node(index));
}

删除指定位置的元素:

    /**
     * Removes the element at the specified position in this list.  Shifts any
     * subsequent elements to the left (subtracts one from their indices).
     * Returns the element that was removed from the list.
     *
     * @param index the index of the element to be removed
     * @return the element previously at the specified position
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }

其它位置的方法:

/**
 * 判断参数是否是已存在元素的索引。
 */
private boolean isElementIndex(int index) {
    // 返回true如果索引大于等于0且小于size,否则返回false
    return index >= 0 && index < size;
}

/**
 * 判断参数是否是迭代器或添加操作的有效位置的索引。
 */
private boolean isPositionIndex(int index) {
    // 返回true如果索引大于等于0且小于等于size,否则返回false
    return index >= 0 && index <= size;
}

/**
 * 构造一个IndexOutOfBoundsException异常的详细消息。
 * 在众多可能的错误处理代码重构中,这种“轮廓化”在服务器和客户端VMs上的性能表现最佳。
 */
private String outOfBoundsMsg(int index) {
    // 返回索引和大小信息的字符串
    return "索引: " + index + ", 大小: " + size;
}

/**
 * 检查元素索引是否有效。
 */
private void checkElementIndex(int index) {
    // 如果索引不是有效元素索引,抛出IndexOutOfBoundsException异常
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

/**
 * 检查位置索引是否有效。
 */
private void checkPositionIndex(int index) {
    // 如果索引不是有效位置索引,抛出IndexOutOfBoundsException异常
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

2.9 查找操作

查找操作的本质是查找元素的下标:

查找第一次出现的index, 如果找不到返回-1;

/**
 * 返回此列表中指定元素的第一次出现的索引,如果此列表不包含该元素,则返回-1。
 * 更正式地说,返回最低索引{@code i},
 * 如果没有这样的索引,则返回-1。
 *
 * @param o 要搜索的元素
 * @return 指定元素在此列表中第一次出现的索引,如果此列表不包含该元素,则返回-1
 */
public int indexOf(Object o) {
    int index = 0; // 初始化索引变量
    if (o == null) { // 如果要查找的元素为null
        for (Node<E> x = first; x != null; x = x.next) { // 遍历链表
            if (x.item == null) // 如果当前节点的元素为null
                return index; // 返回当前索引
            index++; // 索引递增
        }
    } else { // 如果要查找的元素不为null
        for (Node<E> x = first; x != null; x = x.next) { // 遍历链表
            if (o.equals(x.item)) // 如果当前节点的元素等于要查找的元素
                return index; // 返回当前索引
            index++; // 索引递增
        }
    }
    return -1; // 如果没有找到元素,返回-1
}

查找最后一次出现的index, 如果找不到返回-1;

    /**
     * Returns the index of the last occurrence of the specified element
     * in this list, or -1 if this list does not contain the element.
     * More formally, returns the highest index {@code i} such that
     * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>,
     * or -1 if there is no such index.
     *
     * @param o element to search for
     * @return the index of the last occurrence of the specified element in
     *         this list, or -1 if this list does not contain the element
     */
    public int lastIndexOf(Object o) {
        int index = size;
        if (o == null) {
            for (Node<E> x = last; x != null; x = x.prev) {
                index--;
                if (x.item == null)
                    return index;
            }
        } else {
            for (Node<E> x = last; x != null; x = x.prev) {
                index--;
                if (o.equals(x.item))
                    return index;
            }
        }
        return -1;
    }

2.10 Queue 方法

    /**
     * 获取但不移除列表的头部(第一个元素)。
     *
     * @return 列表的头部,如果列表为空则返回{@code null}
     * @since 1.5
     */
    public E peek() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
    }

    /**
     * 获取但不移除列表的头部(第一个元素)。
     *
     * @return 列表的头部
     * @throws NoSuchElementException 如果列表为空
     * @since 1.5
     */
    public E element() {
        return getFirst();
    }

    /**
     * 获取并移除列表的头部(第一个元素)。
     *
     * @return 列表的头部,如果列表为空则返回{@code null}
     * @since 1.5
     */
    public E poll() {
        final Node<E> f = first;
        return (f == null) ? null : unlinkFirst(f);
    }

    /**
     * 获取并移除列表的头部(第一个元素)。
     *
     * @return 列表的头部
     * @throws NoSuchElementException 如果列表为空
     * @since 1.5
     */
    public E remove() {
        return removeFirst();
    }

    /**
     * 将指定元素添加到列表的尾部(最后一个元素)。
     *
     * @param e 要添加的元素
     * @return {@code true}(由{@link Queue#offer}指定)
     * @since 1.5
     */
    public boolean offer(E e) {
        return add(e);
    }

    // ... 其他方法省略 ...

2.11 Deque 方法

/**
     * 在列表的前端插入指定的元素。
     *
     * @param e 要插入的元素
     * @return {@code true}(由{@link Deque#offerFirst}指定)
     * @since 1.6
     */
    public boolean offerFirst(E e) {
        addFirst(e);
        return true;
    }
    /**
     * 在列表的末端插入指定的元素。
     *
     * @param e 要插入的元素
     * @return {@code true}(由{@link Deque#offerLast}指定)
     * @since 1.6
     */
    public boolean offerLast(E e) {
        addLast(e);
        return true;
    }
    /**
     * 检索但不移除列表的第一个元素,如果列表为空则返回{@code null}。
     *
     * @return 列表的第一个元素,如果列表为空则返回{@code null}
     * @since 1.6
     */
    public E peekFirst() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
     }
    /**
     * 检索但不移除列表的最后一个元素,如果列表为空则返回{@code null}。
     *
     * @return 列表的最后一个元素,如果列表为空则返回{@code null}
     * @since 1.6
     */
    public E peekLast() {
        final Node<E> l = last;
        return (l == null) ? null : l.item;
    }
    /**
     * 检索并移除列表的第一个元素,如果列表为空则返回{@code null}。
     *
     * @return 列表的第一个元素,如果列表为空则返回{@code null}
     * @since 1.6
     */
    public E pollFirst() {
        final Node<E> f = first;
        return (f == null) ? null : unlinkFirst(f);
    }
    /**
     * 检索并移除列表的最后一个元素,如果列表为空则返回{@code null}。
     *
     * @return 列表的最后一个元素,如果列表为空则返回{@code null}
     * @since 1.6
     */
    public E pollLast() {
        final Node<E> l = last;
        return (l == null) ? null : unlinkLast(l);
    }
    /**
     * 将元素推入由该列表表示的堆栈。换句话说,将元素插入到列表的前端。
     *
     * <p>这个方法等同于{@link #addFirst}。
     *
     * @param e 要推送的元素
     * @since 1.6
     */
    public void push(E e) {
        addFirst(e);
    }
    /**
     * 从由该列表表示的堆栈中弹出一个元素。换句话说,移除并返回列表的第一个元素。
     *
     * <p>这个方法等同于{@link #removeFirst()}。
     *
     * @return 列表前端的元素(即由该列表表示的堆栈的顶部)
     * @throws NoSuchElementException 如果列表为空
     * @since 1.6
     */
    public E pop() {
        return removeFirst();
    }
    /**
     * 从头到尾遍历列表,移除列表中第一次出现的指定元素。如果列表不包含该元素,则列表不变。
     *
     * @param o 要从列表中移除的元素,如果存在
     * @return 如果列表包含指定的元素则返回{@code true}
     * @since 1.6
     */
    public boolean removeFirstOccurrence(Object o) {
        return remove(o);
    }
    /**
     * 从头到尾遍历列表,移除列表中最后一次出现的指定元素。如果列表不包含该元素,则列表不变。
     *
     * @param o 要从列表中移除的元素,如果存在
     * @return 如果列表包含指定的元素则返回{@code true}
     * @since 1.6
     */
    public boolean removeLastOccurrence(Object o) {
        if (o == null) {
            for (Node<E> x = last; x != null; x = x.prev) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
            for (Node<E> x = last; x != null; x = x.prev) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }

二、小林-图解网络-TCP面试题

1、Socket 编程

1.1 针对 TCP 应该如何 Socket 编程?

基于 TCP 协议的客户端和服务端工作

  • 服务端和客户端初始化 socket,得到文件描述符;
  • 服务端调用 bind,将 socket 绑定在指定的 IP 地址和端口;
  • 服务端调用 listen,进行监听;
  • 服务端调用 accept,等待客户端连接;
  • 客户端调用 connect,向服务端的地址和端口发起连接请求;
  • 服务端 accept 返回用于传输的 socket 的文件描述符;
  • 客户端调用 write 写入数据;服务端调用 read 读取数据;
  • 客户端断开连接时,会调用 close,那么服务端 read 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭。

这里需要注意的是,服务端调用 accept 时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。

所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket

成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。

1.2 listen 时候参数 backlog 的意义?

Linux内核中会维护两个队列:

  • 半连接队列(SYN 队列):接收到一个 SYN 建立连接请求,处于 SYN_RCVD 状态;
  • 全连接队列(Accpet 队列):已完成 TCP 三次握手过程,处于 ESTABLISHED 状态;

 SYN 队列 与 Accpet 队列

int listen (int socketfd, int backlog)
  • 参数一 socketfd 为 socketfd 文件描述符
  • 参数二 backlog,这参数在历史版本有一定的变化

在早期 Linux 内核 backlog 是 SYN 队列大小,也就是未完成的队列大小。

在 Linux 内核 2.2 之后,backlog 变成 accept 队列,也就是已完成连接建立的队列长度,所以现在通常认为 backlog 是 accept 队列。

但是上限值是内核参数 somaxconn 的大小,也就说 accpet 队列长度 = min(backlog, somaxconn)。

1.3 accept 发生在三次握手的哪一步?

我们先看看客户端连接服务端时,发送了什么?

socket 三次握手

  • 客户端的协议栈向服务端发送了 SYN 包,并告诉服务端当前发送序列号 client_isn,客户端进入 SYN_SENT 状态;
  • 服务端的协议栈收到这个包之后,和客户端进行 ACK 应答,应答的值为 client_isn+1,表示对 SYN 包 client_isn 的确认,同时服务端也发送一个 SYN 包,告诉客户端当前我的发送序列号为 server_isn,服务端进入 SYN_RCVD 状态;
  • 客户端协议栈收到 ACK 之后,使得应用程序从 connect 调用返回,表示客户端到服务端的单向连接建立成功,客户端的状态为 ESTABLISHED,同时客户端协议栈也会对服务端的 SYN 包进行应答,应答数据为 server_isn+1;
  • ACK 应答包到达服务端后,服务端的 TCP 连接进入 ESTABLISHED 状态,同时服务端协议栈使得 accept 阻塞调用返回,这个时候服务端到客户端的单向连接也建立成功。至此,客户端与服务端两个方向的连接都建立成功。

从上面的描述过程,我们可以得知客户端 connect 成功返回是在第二次握手,服务端 accept 成功返回是在三次握手成功之后。

1.4 客户端调用 close 了,连接是断开的流程是什么?

我们看看客户端主动调用了 close,会发生什么?

客户端调用 close 过程

  • 客户端调用 close,表明客户端没有数据需要发送了,则此时会向服务端发送 FIN 报文,进入 FIN_WAIT_1 状态;
  • 服务端接收到了 FIN 报文,TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,应用程序可以通过 read 调用来感知这个 FIN 包。这个 EOF 会被放在已排队等候的其他已接收的数据之后,这就意味着服务端需要处理这种异常情况,因为 EOF 表示在该连接上再无额外数据到达。此时,服务端进入 CLOSE_WAIT 状态;
  • 接着,当处理完数据后,自然就会读到 EOF,于是也调用 close 关闭它的套接字,这会使得服务端发出一个 FIN 包,之后处于 LAST_ACK 状态;
  • 客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进入 TIME_WAIT 状态;
  • 服务端收到 ACK 确认包后,就进入了最后的 CLOSE 状态;
  • 客户端经过 2MSL 时间之后,也进入 CLOSE 状态;

1.5 没有 accept,能建立 TCP 连接吗?

答案:可以的

accpet 系统调用并不参与 TCP 三次握手过程,它只是负责从 TCP 全连接队列取出一个已经建立连接的 socket,用户层通过 accpet 系统调用拿到了已经建立连接的 socket,就可以对该 socket 进行读写操作了。

半连接队列与全连接队列

1.6 没有 listen,能建立 TCP 连接吗?

答案:可以的

客户端是可以自己连自己的形成连接(TCP自连接),也可以两个客户端同时向对方发出请求建立连接(TCP同时打开),这两个情况都有个共同点,就是没有服务端参与,也就是没有 listen,就能 TCP 建立连接。