对于一个tcp连接,在c语言里一般有2种方法可以将其关闭:

close(sock_fd);

或者

shutdown(sock_fd, ...);

多数情况下这2个方法的效果没有区别,可以互换使用。除了:

nix系统里socket是1个文件,但文件不1定是1个socket;

所以在进入系统调用后和达到协议层前(发出FIN包这一段), close()和shutdown()的行为会有1点差异。

到达协议层以后,close()和shutdown()没有区别。

举几个栗子示范下close()和shutdown()的差异

下面通过几个例子演示下close()和shutdown()在多线程并发时的行为差异, 我们假设场景是:

栗子1: socket阻塞在recv()上, 调用close()

// Close a waiting recv()
Time
 |
 |  thread-1                  | thread-2           | tcpdump
 |                            |                    |
 |  recv(sock_fd              |                    |
 |      <unfinished ...>      |                    |
1|                            | close(sock_fd) = 0 |
 |                            |                    | // Some data arrived
 |                            |                    | // after close()
2|                            |                    | < seq 1:36 ... length 35
 |                            |                    | > ack 36 ...
 |  // Data was received.     |                    |
3|  <... recv resumed>) = 35  |                    |
4|                            |                    | > FIN sent
 |                            |                    | < ack of FIN received
 |                            |                    | ...
 |  // Can't be used any more |                    |
5v  recv(sock_fd) = -1        |                    |

在上面的例子里:

可以看到,close()没有立即关闭socket的连接,也没有打断等待的recv()。

栗子2: socket阻塞在recv()上, 调用shutdown()

// Shutdown a waiting recv()
Time
 |
 |  thread-1                  | thread-2              | tcpdump
 |                            |                       |
 |  recv(sock_fd              |                       |
 |      <unfinished ...>      |                       |
1|                            | shutdown(sock_fd) = 0 | > FIN sent
 |                            |                       | < ack of FIN received
 |                            |                       | ...
 |  // Woken up by shutdown() |                       |
 |  // no errno set           |                       |
2|  <... recv resumed>) = 0   |                       |
 v                            |                       |

在上面的例子里:

可以看到,shutdown()和close()不同, 会立即关闭socket的连接,并唤醒等待的recv()。

以上2个例子的代码

close-or-shutdown-recv

栗子3: socket阻塞在accept()上, 调用shutdown()

类似的,对阻塞在accept()上的socket调用shutdown(),accept也会被唤醒:

// Shutdown a waiting accept()
Time
 |
 |  thread-1                      | thread-2
 |                                |
 |  accept(sock_fd                |
 |      <unfinished ...>          |
1|                                | shutdown(sock_fd) = 0
 |                                |
 |  // Woken up by shutdown()     |
 |  // errno set to EINVA         |
2|  <... accept resumed>) = -1    |
 |                                |
 v                                |

结论

现在大部分网络应用都使用nonblocking socket和事件模型如epoll的时候, 因为nonblocking所以没有线程阻塞, 上面提到的行为差别不会体现出来 。


go中不能唤醒的问题和重现方法

(开始写的时候没有记清楚重现步骤,感谢 [foxmailed][foxmailed] 提醒。)

上面的描述不准确,更新一下, 实际上是2个问题在1起引起的TCPListener.Close无法唤醒Accept的goroutine:

.File()在我们的代码里用在进程重启过程中的监听fd的继承.

为了解决这个问题, 我们在代码里每次调用.File()后,都加上了1句修正:

syscall.SetNonblock( int(f.Fd()), true )

下面这段代码可以重现go中Close不唤醒的问题:

close-does-not-wake-up-accept.go

package main
import (
	"log"
	"net"
	"runtime"
	"time"
)
func main() {
	runtime.GOMAXPROCS(2)
	l, err := net.Listen("tcp", ":2000")
	if err != nil {
		log.Fatal(err)
	}

	show_bug := true
	if show_bug {
		// TCPListener.File() calls dup() that switches the fd to blocking
		// mode
		l.(*net.TCPListener).File()
	}

	go func() {
		log.Println("listening... expect an 'closed **' error in 1 second")
		_, e := l.Accept()
		log.Println(e)
	}()
	time.Sleep(time.Second * 1)
	l.Close()
	time.Sleep(time.Second * 1)
}