一、Java全栈知识体系-LinkedList源码解析
1、概述
LinkedList同时实现了List接口和Deque接口,也就是说它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(stack)。这样看来,LinkedList简直就是个全能冠军。当你需要使用栈或者队列时,可以考虑使用LinkedList,一方面是因为Java官方已经声明不建议使用Stack类,更遗憾的是,Java里根本没有一个叫做Queue的类(它是个接口名字)。关于栈或队列,现在的首选是ArrayDeque,它有着比LinkedList(当作栈或队列使用时)有着更好的性能。
LinkedList的实现方式决定了所有跟下标相关的操作都是线性时间,而在首段或者末尾删除元素只需要常数时间。为追求效率LinkedList没有实现同步(synchronized),如果需要多个线程并发访问,可以先采用Collections.synchronizedList()方法对其进行包装。
2、LinkedList实现
2.1 底层数据结构
LinkedList底层通过双向链表实现。双向链表的每个节点用内部类Node表示。LinkedList通过first和last引用分别指向链表的第一个和最后一个元素。注意这里没有所谓的dummy,当链表为空的时候first和last都指向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)。
删除元素 - 指的是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++;
}
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 ? get(i)==null : 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 编程?
- 服务端和客户端初始化
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 状态;
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 发生在三次握手的哪一步?
我们先看看客户端连接服务端时,发送了什么?
- 客户端的协议栈向服务端发送了 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,表明客户端没有数据需要发送了,则此时会向服务端发送 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 建立连接。