listen()、accept()和connect()、三者之间的关系
基于 TCP 的网络编程开发分为服务器端和客户端两部分,常见的核心步骤和流程如下:
listen()函数
对于服务器,它是被动连接的。
举一个生活中的例子,通常的情况下,移动的客服(相当于服务器)是等待着客户(相当于客户端)电话的到来。而这个过程,需要调用listen()函数。
listen() 函数的主要作用就是将套接字( sockfd )变成被动的连接监听套接字(被动等待客户端的连接),至于参数 backlog 的作用是设置内核中连接队列的长度(这个长度有什么用,后面做详细的解释),TCP 三次握手也不是由这个函数完成,listen()的作用仅仅告诉内核一些信息。
listen()函数不会阻塞,它主要做的事情为,将该套接字和套接字对应的连接队列长度告诉 Linux 内核,然后,listen()函数就结束。
这样的话,当有一个客户端主动连接(connect()),Linux 内核就自动完成TCP 三次握手,将建立好的链接自动存储到队列中,如此重复。
所以,只要 TCP 服务器调用了 listen(),客户端就可以通过 connect() 和服务器建立连接,而这个连接的过程是由内核完成。
accept()函数
accept()函数功能是,从处于 established 状态的连接队列头部取出一个已经完成的连接。
如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。
connect()函数
对于客户端的 connect() 函数,该函数的功能为客户端主动连接服务器。
建立连接是通过三次握手,而这个连接的过程是由内核完成,不是这个函数完成的。
这个函数的作用仅仅是通知 Linux 内核,让 Linux 内核自动完成 TCP 三次握手连接,最后把连接的结果返回给这个函数的返回值(成功连接为0, 失败为-1)。
通常的情况,客户端的 connect() 函数默认会一直阻塞,直到三次握手成功或超时失败才返回(正常的情况,这个过程很快完成)。
Golang中的TCP网络编程
socket相关的底层函数
//创建一个socket文件描述符 套接字
socketFunc func(int, int, int) (int, error) = syscall.Socket
//绑定一个本机IP:port到socket文件描述符上
func Bind(fd int, sa Sockaddr) (err error)
//监听是否有tcp连接请求
listenFunc func(int, int) error = syscall.Listen
//获取一个建立好的tcp连接 默认阻塞
acceptFunc func(int) (int, syscall.Sockaddr, error) = syscall.Accept
//发起tcp连接请求
connectFunc func(int, syscall.Sockaddr) error = syscall.Connect
//关闭连接
closeFunc func(int) error = syscall.Close
Dial()函数
Dial() 函数用于创建网络连接,函数原型如下:
func Dial(network,address string) (Conn,error) {
var d Dialer
return d.Dial(network,address)
}
参数说明如下:
- network 参数表示传入的网络协议(比如 tcp、udp 等);
- address 参数表示传入的 IP 地址或域名,而端口号是可选的,如果需要指定的话,以:的形式跟在地址或域名的后面即可。如果连接成功,该函数返回连接对象,否则返回 error。
成功建立连接后,就可以进行数据的发送和接收。
发送数据时使用连接对象 conn 的 Write() 方法,接收数据时使用 Read() 方法。
实际上,Dial() 函数是对 DialTCP()、DialUDP()、DialIP()、DialUnix() 函数的封装,同时实现了Conn接口。
func Dial(network, address string) (Conn, error)
func DialTCP(net string,laddr,raddr *TCPAddr) (c *TCPConn,err error)
func DialUDP(net string,raddr *UDPAddr) (c *UDPConn,err error)
func DialIP(netProto string,raddr *IPAddr) (c *IPConn,err error)
func DialUnix(net string,raddr *UnixAddr) (c *UnixConn,err error)
虽然返回TCPConn,UDPConn,IPConn,UnixConn,但是他们的底层是一样的如下图:
Dial() 函数支持如下几种网络协议:
- tcp、tcp4(仅限 IPv4)、tcp6(仅限 IPv6)
- udp、udp4(仅限 IPv4)、udp6(仅限 IPv6)
- ip、ip4(仅限 IPv4)、ip6(仅限 IPv6)
- unix、unixgram 和 unixpacket
几种常见协议的调用方式
// TCP 连接
conn,err := net.Dial("tcp","192.168.10.10:80")
// UDP 连接:
conn,err := net.Dial("udp","192.168.10.10:8888")
// ICMP 连接(使用协议名称):
// 提示:ip4 表示 IPv4,相应的 ip6 表示 IPv6
conn,err := net.Dial("ip4:icmp","c.biancheng.net")
// ICMP 连接(使用协议编号):
conn,err := net.Dial("ip4:1","10.0.0.3")
Demo
服务端 server.go
package main
import (
"fmt"
"net"
)
func main() {
// 服务端监听
lis, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println("err: ", err)
return
}
defer lis.Close()
// 阻塞等待客户端连接
conn, err := lis.Accept()
if err != nil {
fmt.Println("err: ", err)
return
}
// 接收用户请求
buf := make([]byte, 1024) // 缓冲区大小 1024byte
n, err := conn.Read(buf)
if err != nil {
fmt.Println("err: ", err)
return
}
fmt.Println("buf: ", string(buf[:n]))
// 关闭当前用户连接
defer conn.Close()
}
启动服务端:go run server.go
HSD:tcp daysun$ go run server.go
buf: Are you ok?
客户端发送数据
HSD:~ daysun$ nc 127.0.0.1 8000
Are you ok?
HSD:~ daysun$
客户端 client.go
package main
import (
"fmt"
"net"
)
func main() {
// 主动连接服务器
conn, err := net.Dial("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println("连接服务器失败: ", err)
return
}
fmt.Println("连接服务器成功")
defer conn.Close()
str := []byte("Are you ok?")
fmt.Println("发送数据: ", str)
// 发送数据
conn.Write(str)
}
go run server.go
go run client.go
并发Demo
server.go
package main
import (
"fmt"
"net"
"strings"
)
//处理用户请求
func HandleConn(conn net.Conn) {
//函数调用完毕,自动关闭conn
defer conn.Close()
//获取客户端的网络地址信息
addr := conn.RemoteAddr().String()
fmt.Println("客户端连接成功:", addr)
buf := make([]byte, 2048)
for {
//读取用户数据
n, err := conn.Read(buf)
if err != nil {
fmt.Println("err = ", err)
return
}
fmt.Printf("接收到数据 [%s]: %s", addr, string(buf[:n]))
//if "exit" == string(buf[:n-1]) { //nc测试
if "exit" == string(buf[:n-2]) { //自己写的客户端测试, 发送时,多了2个字符, "\r\n"
fmt.Println(addr, " exit")
return
}
//把数据转换为大写,再给用户发送
conn.Write([]byte(strings.ToUpper(string(buf[:n]))))
}
}
func main() {
//监听
listener, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println("err = ", err)
return
}
defer listener.Close()
//接收多个用户
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("err = ", err)
return
}
//处理用户请求, 新建一个协程
go HandleConn(conn)
}
}
client.go
package main
import (
"fmt"
"net"
"os"
)
func main() {
//主动连接服务器
conn, err := net.Dial("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println("net.Dial err = ", err)
return
}
//main调用完毕,关闭连接
defer conn.Close()
fmt.Println("请输入内容:")
go func() {
//从键盘输入内容,给服务器发送内容
str := make([]byte, 1024)
for {
strLen, err := os.Stdin.Read(str) //从键盘读取内容, 放在str
if err != nil {
fmt.Println("os.Stdin. err = ", err)
return
}
//把输入的内容给服务器发送
conn.Write(str[:strLen])
}
}()
//接收服务器回复的数据
//切片缓冲
buf := make([]byte, 1024)
for {
n, err := conn.Read(buf) //接收服务器的请求
if err != nil {
fmt.Println("conn.Read err = ", err)
return
}
// 打印接收到的内容, 转换为字符串再打印
fmt.Println("接收服务端内容:", string(buf[:n]))
}
}