本章内容:
三次握手:客户端发 syn 数据包——服务器以 syn-ack 进行响应——客户端发送 ACK 数据包,成功建立连接。
如何判断一个端口的状态(开放、关闭、过滤)?
开放:进行正常的三次握手。
关闭:服务器以 rst 数据包进行响应,而不是 syn-ack。
过滤(流量被防火墙过滤):服务器端没有任何响应。
端口转发:使用中间系统代理连接绕过或穿透防火墙
如企业内某客户端,想要访问被防火墙过滤的恶意网址 b.com,那么客户端就可以先访问没有被防火墙过滤的 a.com,在 a.com设置中间代理,将流量代理到 b.com,那么客户端就能够绕过客户端成功访问了 b.com

Go 的**net**包:**func Dial(network,address string) (Conn,error)**
第一个参数用于标识要启动的连接类型,是一个字符串,有
"tcp","tcp4"(IPv4-only),"tcp6"(IPv6-only),"udp","udp4"(IPv4-only),"udp6"(IPv6-only),"ip","ip4"(IPv4-only),"ip6"(IPv6-only),"unix","unixgram"and"unixpacket"第二个参数是想要连接的主机,对于 TCP/UDP 连接,使用
host:port形式,host 可以是域名、ip 地址、IPv6 地址,port 可以是端口名或服务名,如Dial("tcp", "golang.org:http") Dial("tcp", "192.0.2.1:http") Dial("tcp", "198.51.100.1:80") Dial("udp", "[2001:db8::1]:domain") Dial("udp", "[fe80::1%lo0]:53") Dial("tcp", ":80")
通过检查返回的 error参数来判断连接是否成功。
// 扫描 1-1024 端口
// fmt.Sprintf()格式化字符串
func main() {
for i:= 1;i <= 1024;i++ {
addr:= fmt.Sprintf("scanme.nmap.org:%d",i)
fmt.Println(addr)
conn,err := net.Dial("tcp",addr)
if err!= nil {
fmt.Println("连接失败")
continue
}
fmt.Println(i,"号端口连接成功")
conn.Close() // 关闭连接
}
}
解决 main goroutine 不等待其他 goroutine 的问题
time.sleep()等待函数,等待其他 goroutine WaitGroup
WaitGroup结构体字段不可导出,该结构体有三个方法
Add(int):该方法会按所提供的数字递增内部计数器
Done():将内部计数器减 1
Wait():阻止调用它的 goroutine 执行,并在内部计数器达到零之前不允许进一步执行(即进入等待状态)
func main() {
var wg sync.WaitGroup
for i:= 1;i <= 1024;i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
addr:= fmt.Sprintf("scanme.nmap.org:%d",i)
//fmt.Println(addr)
conn,err := net.Dial("tcp",addr)
if err!= nil {
return
}
fmt.Printf("端口%d:连接成功\n",i)
conn.Close()
}(i)
}
wg.Wait()
}
首先声明 WaitGroup 变量,在循环中每创建一个 goroutine 就把内部计数器+1,在每个 goroutine 中,若是连接失败立即返回,因此需要使用延迟调用 defer 来-1,main goroutine 中使用等待函数,当其他 goroutine 运行完毕,才结束主程序。
通过这种方法进行扫描会出现结果不一样的问题,通过工人池的方法可以解决
使用 for 循环创建一定数量的工人 goroutine 作为资源池,然后在 main goroutine 中使用通道提供工作
注释仔细看!
func worker(ports chan int,wg *sync.WaitGroup) {
//为什么在此要用循环
//因为创建的一百个工人 goroutine 只要没有 return 返回就会一直存在
//若不使用循环,那么工人只会干一次就罢工(还没死)也就是一百个工人只能扫描一百个端口,而 wg.done 也只能触发 100 次,而主程序中 wg.add 执行了 1024 次,程序永远不会停止
//使用循环才会使工人一直干,直到工程完毕,工人才会放假,wg.wait 才能使 main 结束等待,程序结束
for i:= range ports {
addr:= fmt.Sprintf("127.0.0.1:%d",i)
conn,err := net.Dial("tcp",addr)
if err!= nil {
//fmt.Printf("端口%d:连接失败\n",i)
// 在工人池的 goroutine 中不能 return,因为 return 了,goroutine 就会销毁,而工人池中一共一百个 goroutine,会消耗掉
//return
} else {
fmt.Printf("端口%d:连接成功\n",i)
// 连接成功了才能关闭,否则 conn 没有值,访问空地址,产生 panic
conn.Close()
}
// 此处的 wg.done 不能用 defer,因为只有函数返回时才会调用 defer,而我们要求工人 goroutine 一直存活,因此 defer 永远不会被调用
wg.Done()
}
}
func main() {
var wg sync.WaitGroup
ports:= make(chan int,100)
//创建工人池,其中包含一百个蓄势待发的工人 goroutine
for i:= 0;i < cap(ports);i++ {
go worker(ports, &wg)
}
//通过管道按需分配 goroutine,让工人工作
for i:= 1;i <= 1024;i++ {
wg.Add(1)
ports <- i
}
wg.Wait()
close(ports)
}
//输出
端口 135:连接成功
端口 445:连接成功
端口 902:连接成功
端口 912:连接成功
以上打印出来的端口是乱序的,如何按照端口号进行顺序输出?
func worker(ports chan int,results chan<- result) {
for i:= range ports {
addr:= fmt.Sprintf("127.0.0.1:%d",i)
conn,err := net.Dial("tcp",addr)
if err!= nil {
results <- result{i,false}
} else {
results <- result{i,true}
conn.Close()
}
}
}
type result struct {
port int
isTrue bool
}
func main() {
maxPort:= 1024
results:= make(chan result)
ports:= make(chan int,100)
var s []int
//创建工人池,其中包含一百个蓄势待发的工人 goroutine
for i:= 0;i < cap(ports);i++ {
go worker(ports,results)
}
//通过管道按需分配 goroutine,让工人工作
// 这里为什么需要创建 goroutine
//若不用 goroutine,那么由于 ports 管道容量为 100,而有 1024 个端口,而工人 goroutine 不断往 results 管道装东西,但是没有接收者取东西,results 管道一直是满的状态,所以工人 goroutine 就会陷入阻塞,
// 导致 ports 中的内容也不能被接收,所以 main goroutine 也会陷入阻塞,程序就卡住了
//若不使用 goroutine,只要将 results 管道的容量改为>=端口的数量,程序将会正常运行。
//而使用 goroutine,results 中的内容被 main goroutine 不断地接收,ports 中的内容就能不断被工人接收,所以程序也不会陷入阻塞,就能正常运行
go func() {
for i:= 1;i <= maxPort;i++ {
ports <- i
}
}()
//由于一共 1024 个端口,每扫描一次都会往 results 中发送,所以用 1024 次循环来接收
for i:= 0;i < maxPort;i++ {
result:= <-results
if result.isTrue {
s = append(s,result.port)
}
}
//对其中的元素进行排序
sort.Ints(s)
for i:= 0;i < len(s);i++ {
fmt.Printf("端口%d:连接成功\n",s[i])
}
close(ports)
close(results)
}
以上代码不需要使用 Waitgroup进行同步,因为通过无缓存的 channel 就能完成同步。
通过使用通道将结果传递给切片,并对切片进行处理,最后集中打印,就能使数据有序
不止是无缓存通道会阻塞,若有缓存通道已经满了,而无人接收,那么再发送也会阻塞
io.Reader io.Writer
//io.Reader
type Reader interface {
Read(p []byte) (n int,err error)
}
//io.Writer
type Writer interface {
Write(p []byte) (n int,err error)
}
Reader 和 Writer 是接口类型,不能直接实例化,需要实现其中的方法
Reader 接口用于包装基本的读取方法。
Read 方法读取 len(p)字节数据写入 p。它返回写入的字节数和遇到的任何错误。即使 Read 方法返回值 n < len(p),本方法在被调用时仍可能使用 p 的全部长度作为暂存空间。如果有部分可用数据,但不够 len(p)字节,Read 按惯例会返回可以读取到的数据,而不是等待更多数据。
当 Read 在读取 n > 0 个字节后遭遇错误或者到达文件结尾时,会返回读取的字节数。它可能会在该次调用返回一个非 nil 的错误,或者在下一次调用时返回 0 和该错误。一个常见的例子,Reader 接口会在输入流的结尾返回非 0 的字节数,返回值 err == EOF 或 err == nil。但不管怎样,下一次 Read 调用必然返回(0,EOF)。调用者应该总是先处理读取的 n > 0 字节再处理错误值。这么做可以正确的处理发生在读取部分数据后的 I/O 错误,也能正确处理 EOF 事件。
如果 Read 的某个实现返回 0 字节数和 nil 错误值,表示被阻碍;调用者应该将这种情况视为未进行操作。
Writer 接口用于包装基本的写入方法。
Write 方法 len(p) 字节数据从 p 写入底层的数据流。它会返回写入的字节数(0 <= n <= len(p))和遇到的任何导致写入提取结束的错误。Write 必须返回非 nil 的错误,如果它返回的 n < len(p)。Write 不能修改切片 p 中的数据,即使临时修改也不行。
Conn接口代表通用的面向流的网络连接
type Conn interface {
// Read 从连接中读取数据
// Read 方法可能会在超过某个固定时间限制后超时返回错误,该错误的 Timeout()方法返回真
Read(b []byte) (n int,err error)
// Write 从连接中写入数据
// Write 方法可能会在超过某个固定时间限制后超时返回错误,该错误的 Timeout()方法返回真
Write(b []byte) (n int,err error)
// Close 方法关闭该连接
// 并会导致任何阻塞中的 Read 或 Write 方法不再阻塞并返回错误
Close()error
// 返回本地网络地址
LocalAddr()Addr
// 返回远端网络地址
RemoteAddr()Addr
// 设定该连接的读写 deadline,等价于同时调用 SetReadDeadline 和 SetWriteDeadline
// deadline 是一个绝对时间,超过该时间后 I/O 操作就会直接因超时失败返回而不会阻塞
// deadline 对之后的所有 I/O 操作都起效,而不仅仅是下一次的读或写操作
// 参数 t 为零值表示不设置期限
SetDeadline(t time.Time)error
// 设定该连接的读操作 deadline,参数 t 为零值表示不设置期限
SetReadDeadline(t time.Time)error
// 设定该连接的写操作 deadline,参数 t 为零值表示不设置期限
// 即使写入超时,返回值 n 也可能>0,说明成功写入了部分数据
SetWriteDeadline(t time.Time)error
}
从连接中读取数据 conn.Read(): Read(b []byte) (n int,err error)
向连接中写入数据 conn.Write(): Write(b []byte) (n int,err error)
net.Listen()监听来自客户端的连接请求 Listener:
func Listen(net,laddr string) (Listener,error)
返回在一个本地网络地址 laddr 上监听的 Listener。网络类型参数 net 必须是面向流的网络:“tcp”、“tcp4”、“tcp6”、“unix"或"unixpacket”
监听器 Listener是一个用于面向流的网络协议的公用的网络监听器接口
type Listener interface {
// Addr 返回该接口的网络地址
Addr()Addr
// Accept 等待并返回下一个连接到该接口的连接
Accept() (c Conn,err error)
// Close 关闭该接口,并使任何阻塞的 Accept 操作都会不再阻塞并返回错误。
Close()error
}
接收客户端的连接 Listener.Accept(): Accept() (c Conn,err error)
服务器流程:监听连接——接受连接——处理连接
// echo is a handler function that simply echoes received data.
func echo(conn net.Conn) {
defer conn.Close()
// Create a buffer to store received data.
b:= make([]byte,512)
for {
// Receive data via conn.Read into a buffer.
size,err := conn.Read(b[0:])
if err!= nil && err!= io.EOF {
log.Println("Unexpected error")
break
}
if err == io.EOF && size == 0 {
log.Println("Client disconnected")
break
}
log.Printf("Received %d bytes: %s",size,string(b))
// Send data via conn.Write.
log.Println("Writing data")
if _,err := conn.Write(b[0:size]);err != nil {
log.Fatalln("Unable to write data")
}
}
}
func main() {
// Bind to TCP port 20080 on all interfaces.
listener,err := net.Listen("tcp", ":20080")
if err!= nil {
log.Fatalln("Unable to bind to port")
}
log.Println("Listening on 0.0.0.0:20080")
for {
// Wait for connection. Create net.Conn on connection established.
conn,err := listener.Accept()
log.Println("Received connection")
if err!= nil {
log.Fatalln("Unable to accept connection")
}
// Handle the connection. Using goroutine for concurrency.
go echo(conn)
}
}