Go 并发、Socket、HTTP 编程

并发编程

1、并行和并发

并行(parallel): 指同一时刻,有多条指令在多个处理器上执行

并发(concurrency): 指同一个时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得宏观上具有多个 进程同时执行的效果,但是微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。(时间片轮转)

Go 从语言层面就支持了并发,同时,并发程序的内存管理有时候是很复杂的,而 Go 语言提供了自动垃圾回收机制。

Go 语言为并发编程而内置了上层 API 基于 CSP(communicating sequential processes,顺序通信进程)模型。这就意味着显示锁都可以避免的,因为 Go 语言通过安全的通道发送和接受数据以实现同步,大大的简化了并发程序的编写。(CSP通信的方式实现同步而不是锁)

一般情况下,一个普通的桌面计算机跑十几二十个线程就有点负载过大了,但是同样这台机器却可以轻松的让成百上千甚至过万个 goroutine 进行资源竞争。

2、goroutine

goroutine 是 Go 并发设计的核心。goroutine 说到底就是协程,但是它比线程更小,十几个 goroutine 可能体现在底层就是五六个线程,Go 语言内部帮你实现了这些 goroutine 之间的内存共享。执行 goroutine 只需极少的栈内存(大概 4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine 比 thread 更易用、更高效、更轻便。

只需要在函数调用语句前添加 go 关键字,就可以创建并发执行单元。开发人员无需了解任何执行细节,调度器会自动将其安排到合适的系统线程上执行。

在并发编程里,我们通常想讲一个过程切分成几块,然后让每个 goroutine 各自负责一块工作。当一个程序启动时,其主函数即在一个单独的 goroutine 中运行,我们叫它 main goroutine。新的 goroutine 会用 go 语句来创建。

runtime.Goexit(): 退出此协程
runtime.GOMAXPROCS(n):设置并行计算的 CPU 核数的最大值,并返回之前的值
runtime.Gosched(): 用于让出 CPU 时间片,让出当前 goroutine 的执行权限,调度器安排其他等待的任务运行,并在下次某个位置从该位置恢复执行。这就像接力赛,A 拍了一会碰到代码 runtime.Gosched() 就把接力棒交给 B 了,A 歇着 B 继续跑。

注意:

  • 主协程退出了,其他子协程也要跟着退出
  • 有可能主协程退出了,但是子协程还没来得及调用
package main

import (
    "fmt"
    "time"
    "runtime"
)

func test() {
    defer fmt.Println("aaa") // 终止协程,该语句仍然会被执行
    
    // return // 终止此函数
    runtime.Goexit() // 终止坐在的协程
    
    fmt.Println("bbb")
}

func NewTask() {
    // do something
}

func main() {
    go newTask() // 新建一个协程,新建一个任务(只要看见一个 go 便创建一个协程)
    
    // do something
}

多任务很容易出现资源竞争,就需要channel 做协程同步

2、channel

本质上就是一个管道

价值:

  • 使用通信来共享数据
  • 使用通信来同步

goroutine 运行在相同的地址空间,因此访问共享内存必须做好同步。gotoutine 奉行通过 CSP(通信) 来共享内存,而不是共享内存来通信。

引用类型 channel 是 CSP 模式的具体实现,用于多个 goroutine 通讯。其内部实现了同步,确保并发安全。

和 map 类似,channel 也一个对应 make 创建的底层数据结构的引用。

当我们复制一个 channel 或用于函数参数传递时,我们只是拷贝了一个 channel 的引用,因此调用者何时被调用者将引用同一个 channel 对象。和其他的引用类型一样,channel 的零值也是 nil。

定义一个 channel 时,也需要定义发送到 channel 的值的类型。channel 可以使用内置的 make() 函数来创建:

make (chan Type) // 无缓冲区,等价于 make (chan Type, 0)
make (chan Type, capacity) // 有缓冲区

当 capacity = 0 时,channel 是无缓冲阻塞读写的,当 capacity > 0 时,channel 有缓冲、是非阻塞的,直到写满 capacity 个元素才阻塞写入。

channel 操作符 <- 来接收和发送数据,发送和接收数据语法:

channel <- value // 发送 value 到 channel
<-channel // 接收并将其丢弃
x := <-channel // 从 channel 中接受数据,并赋值给 x
x, ok := <-channel // 同能同上,同时检查通道是否已关闭或者是否为空

默认情况下,channel 接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得 goroutine 同步变的更加的简单,而不需要显式的 lock

var ch make(chan int) // 定义通道

func person1() {
    Printer("hello")
    ch <- 666 // 给管道写数据,发送
}

func person2() {
    <- ch // 从管道取数据,接收,如果通道没有数据他就会阻塞
    Printer("world")
}

3、无缓冲和有缓冲 channel

无缓冲的通道(unbuffered channel)是值在接受前没有能力保存任何值的通道。

这种类型的通道要求发送 goroutine 和接收 goroutine 同时准备好,才能完成发送和接收操作。如果两个 goroutine 没有同时准备好,通道会导致先执行发送和接收操作的 gotoutine 阻塞等待。

这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作的单独存在。

使用无缓冲通道 goroutine 之间同步

在第 1 步,两个 goroutine 都到达通道,但哪个都没有开始执行或者接收。
在第 2 步,左侧的 goroutine 将它的手伸进了通道,这模拟了向通道发送数据的行为。这是,这个 goroutine 会在通道中被锁住,直到交换完成。
在第 3 步,右侧的 goroutine 将它的手放入通道,这模拟了从通道接收数据。这个 goroutine 一样会在通道中被锁住,直到交换完成。
在第 4 步和第 5 步,进行交换,并最终,在第 6 步,两个 goroutine 将他们的手从通道里拿出来,这模拟了被锁住的 goroutine 得到释放。两个 goroutine 现在都可以去做别的事情了。

无缓冲的 channel 创建格式:

make (chan Type) // 等价于 make(chan Type, 0)

0 表示容量,表示没有缓存,不能存东西

如果没有指定缓冲区容量,那么通道就是同步的,因此会阻塞到发送者准备好发送和接收着准备好接收。

有缓冲的通道在 goroutine 之间同步数据

有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换:有缓冲的通道没有这种保证。

在第 1 步,右侧的 goroutine 正在从通道接收一个值。
在第 2 步,右侧的这个 goroutine 独立完成了接收值的动作,而左侧的 goroutine 正在发送一个新值到通道里。
在第 3 步,左侧的 goroutine 还在向通道发送新值,而右侧的 goroutine 正在从通道接收另外一个值。这个步骤里的两个操作既不是同步的,也不会互相阻塞。
最后,在第 4 步,所有的发送和接收都完成,而通道里还有几个值,也有一些空间可以存更多的值。

有缓冲的 channel 创建格式:

make(chan Type, capacity)

如果给定了一个缓冲区容量,通道就是异步的。只要缓冲区有未使用空间用于发送数据,或还包含可以接收的数据,那么其通信就是无阻塞地进行。

4、channel 关闭

  • channel 不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显示的结束 range 循环之类的,才去关闭 channel;
  • 关闭 channel 后,无法向 channel 再发送数据(引发 panic 错误后导致接收立即返回零值);
  • 关闭 channel 后,可以继续向 channel 接收数据;
  • 对于 nil channel,无论收发都会被阻塞
ch := make(chan int) // 创建一个无缓存的 channel

// 不需要写数据的时候,关闭 channel
cose(ch)

// 如果 ok 为 true,说明管道没有关闭
num, ok := <-ch

5、访问 channel 内容

  • 迭代
  • range
for num := range ch {
    fmt.Println("num =", num)
}

6、单向的 channel

默认情况下,通道是双向的,也就是,既可以往里面发送数据也可以从里面接收数据。

但是,我们经常见一个通道作为参数进行传递而值希望对方是单向使用的,要么只让它发送数据,要么只让它接收数据,这时候我们可以指定通道的方向。

单向 channel 变量的声明非常简单,如下:

var ch1 chan int // ch1 是一个正常的 channel,不是单向的
var ch2 chan<- float64 // ch2 是单向 channel,只用于写 float64 数据
var ch3 <-chan int // ch3 是单向 channel,只用于读取 int 数据
  • chan<- 表示数据进入管道,要把数据写进管道,对于调用者就是输出
  • <-chan 表示数据从管道出来,对于调用者就是得到管道的数据,当然就是输入

可以将 channel 隐式转换为单向队列,只收或只发,不能将单向 channel 转换为普通 channel:

c := make(chan int, 3)

var send chan<- int = c // send-only
var revc <-chan int = c // receive-ony

send <- 1
<- recv

生产者消费者应用:

package main 

import "fmt"

// 此通道只能写,不能读
func producer(in  chan<- int) {
    for i := 0; i < 10; i++ {
        out <- i * I
    }
}

// 此 channel 只能读,不能写
func consumer(out <-chan int) {
    for num := range out {
        fmt>Println("num = ", num)
    }
}

func main() {
    // 创建一个双向通道
    ch := make(chan int)
    
    // 生产者,生产数字,写入 channel
    // 新开一个协程
    go producer(ch) // channel 传参,引用传递
    
    // 消费者,从 channel 读取内容,打印
    consumer(ch)
}

7、Timer / Ticker 定时器

Timer 是一个定时器,代表未来的一个单一事件,你可以告诉 timer 你要等待多长时间,它提供一个 channel,在将来的那个时间那个 channel 提供了一个时间值。

// 创建定时器,2 秒后就会往 time 通道写内容(当前时间)
timer1 := timer.NewTimer(time.Second * 2)
fmt.Println("当前时间: ", time.Now())

// 2s 后,往 timer.C 写数据,有数据后,就可以读取
t := <-timer1.C
fmt.Printf("t= %V\n", t)

延迟:

// 定时2s,阻塞2s,2s后产生事件,往channel写内容
<-time.After(time.Second * 2)

停止和重置:

timer.Stop() // 停止定时器
timer.Reset(1 * time.Second) // 重新设置为 1 秒

Ticker 是一个定时触发的计时器,它会以一个间隔(interval)往 channel 发送一个事件(当前时间),而 channel 的接收者可以以固定的时间间隔从 channel 中读取事件

ticker := time.NewTicker(1 : time.Second)

i := 0

for {
    <-ticker.C
    I++
    fmt.Println("i = ", i)
    
    if i == 5 {
        ticker.Stop()
        break
    }
}_

8、Select

Go 里面提供了一个关键字 select,通过 select 可以监听 channel 上的数据流动。

select 的用法和 switch 语言非常类似,由 select 开始一个新的选择块,每个选择条件由 case 语句来描述。

与 switch 语句可以选择任何可使用相等比较的条件相比,select 有比较多的限制,其中最大的一条限制就是每个 case 语句必须是一个 IO 操作,大致的结构如下:

select {
case <-chan1:
// 如果 chan1 成功读到数据,则进行该 case 处理语句
case chan2 <- 1:
// 如果成功向 chan 2 写入数据,则进行该 case 处理语句
default:
// 如果上面都没有成功,则进入 default 处理流程
}

在一个 select 语句中,Go 语言会按照顺序从头到尾评估每一个发送和接收的语句。

如果其中的任意一语句可以继续执行(即没有被阻塞),那么就从哪些可以执行的语句中任意选择一条来使用。

超时的实现:

import "time"

func main() {
    ch := make(chan int)
    quit := make(chan bool)
    
    // 新开一个协程
    go func() {
        for {
           select {
                case num := <-ch:
                    fmt.Println("num = ", num)
                    case <-time.After(3 * time.Second)
                        fmt.Println("超时")
                        quit <- true
           } 
        }()
    }
    
    for i := 0; i < 5; i++ {
        ch <- I
        time.Sleeep(time.Second)
    }
    
    <-quit
    fmt.Println("程序结束")
}

网络概述、Socket 编程

1、网络协议

协议可以理解为规则,是数据传输和数据的解释的规则。

为了减少协议复杂性,大多数网络模型采用分层来组织。每一层都有自己的功能,每一层利用下一层提供的服务来为上一层提供服务,本层服务的实现细节对上层屏蔽。

越下面的层,越靠近硬件;越上面的层,越靠近用户。至于每一层叫什么名字,其实并不重要(除了辨识会问,哈哈)。但要具体指导每一层的作用。

OSI 七层协议:

  • 物理层:主要是无力设备标准,如网线接口、光线接口。主要作用就是传输比特流,这一层的数据叫做比特。
  • 数据链路层:定义如何让格式化数据数据以帧为单位进行传输,以及如何让控制对无力介质的访问。错误检测和纠正,保证数据的可靠传输。
  • 网络层:位于不同位置两个主机系统提供连接和路径选择。(IP\ICMP\IGMP)
  • 传输层:定义了一些传输数据的协议和端口号(WWW端口80等)。跟端口相关(TCP\UDP)
  • 会话层:建立和维持会话(Session)
  • 表示层:主要是提供格式化的表示和转换数据的服务。数据的压缩、加密、解密等都是在该层完成。
  • 应用层:(FTP\Telnet\NFS)

几个常见协议:

  • ARP: 通过 IP 地址找 MAC 地址
  • RARP: 通过 MAC 地址找 IP 地址
  • IP: 因特网互联协议
  • TCP: Transmission Control Protocol, 是一种面向连接的、可靠的、基于字节流的传输层通信协议
  • UDP: User Datagram Protocol, 是一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。
  • HTTP: 超文本传输协议
  • ICMP: Internet Control Message Protocol, 因特网控制报文协议,用于在 IP主机、路由器之间的控制协议。
  • IGMP: Internet Group Message Protocol,提供互联网多点传送的功能,即将一个 ip 包拷贝到多个 host
TCP / IP 协议

网络通信条件:

  • 网卡:MAC 地址(不需要用户处理):通过 IP 找 MAC
  • 逻辑地址:IP 地址(需要用户指定),为了确定哪个电脑接收
  • 端口:确定哪个程序接收
    • 同一个程序/进程,只能绑定一个端口
    • 不同系统,同一端口对应的程序可能不一样
封包和解包流程

2、Socket 编程

Socket 套接字,起源于 Unix,而 Unix 基本哲学之一就是“一切皆文件“,都可以用”打开 open -> 读写 write/read -> 关闭 close”。网络的 Socket 数据涮出是一种特殊的 IO,Socket 也是一种文件描述符。Socket 也具有一个类似于打开文件的函数调用:Socket(),该函数返回一个整型的 Socket 描述符,随后的连接建立、数据传输等操作都是通过该 Socket 实现的。

常见的 Socket 类型有两种:流式 Socket(SOCK_STREAM)和数据报式 Socket(SOCK_DGRAM)。流式是一种面向连接的 Socket,针对于面向连接的 TCP 服务应用;数据报式 Socket 是一种无连接的 Socket,对应于无连接的 UDP 服务应用。

C/S模型:

  • 客户端(Client): 主动请求服务
  • 服务器(Server):被动提供服务

B/S模型:

  • 浏览器(Browser):html
  • 服务器(Server)

TCP 的 C/S 架构:

TCP 的 C/S 架构

TCP 服务器

package main

import (
    "fmt"
    "net"
)

func main() {
    // 监听
    ln, err := net.Listen("tcp", ":127.0.0.1:8080")
    if err != nil {
        fmt.Println("err = ", err)
        return
    }
    
    defer listener.Close()
    
    // 阻塞等待用户连接
    for {
        conn, err := listener.Accept()
        if err != nil {
           fmt.Println("err = ", err)
           continue
        }
        
        // 接收用户的请求
        buf := make([]byte, 1024)// 定义 1024 大小的缓冲区
        n, err1 := conn.Read(buf)
        if err1 != nil {
            fmt.Println("errr1 = ", err1)
            continue
        }
        
        fmt.Println("buf = ", string(buf[:n]))
    }
    
    defer conn.Close() // 关闭当前用户链接
}

TCP 客户端

package main

import (
    "fmt"
    "net"
)

func main() {
    // 主动连接服务器
    conn, err := net.Dial("tcp", "127.0.0.1:8000")
    if err != nil {
        fmt.Println("err = ", err)
        return
    }
    
    defer conn.Close()
    
    // 发送数据
    conn.Write([]byte("are u ok?"))
}

3、Socket 并发

使用 goroutine 处理多个用户的 Socket 连接

// 处理用户请求
func HanleConn(conn net.Conn) {
    // 函数调用完毕,自动关闭 conn
    defer conn.Close()

    // 获取客户端的网络地址信息
     addr := conn.RemoteAddr().String()
     // print addr
    
     buf := make([]byte, 2048)
     for {
         // 读取用户数据
         n, err := conn.Read(buf)
         if err != nil {
            // print err
            return 
        }
        
        // print n
        
        // 输入 exit 退出连接
        if "exit" == string(buf[:n]) {
            // print addr
            return
        }
        
        // 把数据转换为答谢,再发给用户
        conn.Write([]byte(strings.ToUpper(string(buf[:n]))))
     }
     
}

func main() {
    // 监听
    listen, err := net.Listen("tcp", "127.0.0.1:8000")
    if err != nil {
        // print err
        return
    }
    
    defer listener.Close()
    
    // 接收多个用户的请求
    for {
        conn, err := listener.Accept()
        if err != nil {
            // print err
            return
        }
        
        // 处理用户请求,每来一个请求新建一个协程
        go HandleConn(conn)
    }
}

客户端多任务模式,即可以接受输入,也可以接受服务器 Socket 回复


func main() {
    // 主动连接服务器
    conn, err := net.Dial("tcp", "127.0.0.1:8000")
    if err != nil {
        // print err
        return
    }

    // main 调用完毕,关闭连接
    defer conn.Close()
    
    go func {
        // 从键盘输入内容,给服务器发送内容
        str := nake([]byte, 1024)
        for {
            n, err := os.Stdin.Read(str) // 从键盘读取内容,放在 str
            if err != nil {
                // print err
                return
            }
            
            // 把输入的内容发送给服务器
            conn.Write(str[:n])
        }
    }()

    // 接收服务器回复的数据
    // 切片缓冲
    buf := make([]byte, 1024)
    for {
        n, err := conn.Read(buf) // 接受的服务器的请求
        if err != nil {
            // print err
            return
        }
        
        // print string(buf[:n])
    }
}

4、使用 Socket 实现文件传输

获取文件属性

package main

import (
    "fmt"
    "os"
)

func main() {
    list := os.Args
    if len(list) != 2 {
        // print useage: xxx file
        return
    }
    
    fileName := list[1]
    
    info, rr := os.Stat(fileName) // 获取文件属性
    if err != nil {
        // print err
        return
    }
    
    // print info.Name(), info.Size()
}

文件发送端

package main

import (
    "fmt"
    "os"
    "io"
    "os"
)

func SendFile(path string, conn net.Conn) {
    // 以只读方式打开文件
    f, err := os.Open(path)
    if err != nil {
        // print err
        return
    }
    
    defer f.Close()
    
    // 读文件内容,读多少发送多少
    buf := make([]byte, 1024 * 4)
    for {
        n, err := f.Read(buf)
        if err != nil {
            if err == io.EOF {
                // 文件发送完成
            } else {
                print err
            }
            return
        }
        // 发送内容
        conn.Write(buf[:n]) // 给服务器发送内容
    }
}

func main() {
    // 提示输入文件
    // print
    
    var path string
    fmt.Scan(&path)
    
    fileName := list[1]
    
    info, rr := os.Stat(fileName) // 获取文件属性
    if err != nil {
        // print err
        return
    }
    
    // 主动连接服务器
    conn, err1 := net.Dial("tcp", "127.0.0.1:8000")
    if err1 != nil {
        // print err
        return
    }
    
    defer conn.Close()
    
    var n int
    
    // 给发送方,先发送文件名
    _, err = conn.Write([]byte(info.Name()))
    if err != nil {
        // print err
        return
    }
    
    // 接收对方回复,如果回复ok,说明对方准备好,可以发送文件
    var n int 
    buf := make([]byte, 1024)
    
    n, err = conn.Read(buf)
    if err != nil {
        // print err
        return
    }
    
    if "ok" == buf[:n] {
        // 发送文件内容
        SendFile(path, conn)
    }
    
}

文件接收端

package main

import (
    "fmt"
    "os"
    "io"
    "os"
)

// 接收文件内容
func RecvFile(fileName string, conn net.Conn) {
    // 新建文件
    f, err := os.Create(fileName)
    if err != nil {
        // print err
        return
    }
    
    buf := make([]byte, 1024 * 4)
    
    // 接收多少,写多少
    for {
        n, err := conn.Read(buf)
        if err != nil {
            if err == io.EOF {
                // 文件接收完毕
            } else {
                // print err
            }
            return
        }
        
        if n == 0 {
            // 文件接收完毕
            return
        }
        
        
        // 往文件写入内容
        f.Write(buf[:n])
    }
}

func main() {
    // 监听
    listener, err := net.Listen("tcp", "127.0.0.1:8000")
    if err != nil {
        // print err
        return
    }
    
    defer listenner.Close()
    
    // 阻塞等待用户连接
    conn, err1 := listenner.Accept()
    if err1 != nil {
        // print err1
        return
    }
    
    buf := make([]byte, 1024)
    var n int
    n, err2 = conn.Read(buf) // 读取对方发送的文件名
    if err2 != nil {
        // print errr2
        return
    }
    
    defer conn.Close()
    
    // fileName 保存一下
    fileNmae := stirng(buf[:n])
    
    // 回复 ok
    conn.Write([]byte("ok"))
    
    // 接收文件内容
    Recv(fileName, conn)
}

5、使用 Socket 实现并发聊天室服务器

可以使用 NetCat 工具调试 Socket 连接

设计思路

  • map: 保存在线用户
type Client struct {
    c chan string
    Name string
    Addr string
}

var onlineMap[string] Client

主协程:

  • 处理用户连接
  • 将用户加入 map
  • 告诉所有在线用户,谁上线了
message <- 某个用户上线了

go 新开一个协程:

for {
    msg := <-message // 如果有内容
    // 遍历 map,看有多少个成员
    for _, cli := range onlineMap {
        cli.C <- msg
    }
}

go 专门发送信息:

for msg := range cli.C {
    write(msg)
}

go 专门接收用户的请求,把用户发过来的数据转发。用户发送过来的数据是 buf

mssage <- buf

对方下线,把当前用户从 map 中移除

具体实现

main.go

package main

import (
    "fmt"
    "net"
    "time"
)

type Client struct {
    C chan string // 用于发送数据的管道
    Name string // 用户名
    Addr string // 网络地址
}

// 保存在线用户 cliAddr ===> Client
var onlineMap map[string]Client
// 消息
var massage = make(chan string)

// 新开一个协程,转发消息,只要有消息来了,就遍历 map,给 map 的每个成员发送消息
func Manager() {
    for {
        // 给 map 分配空间
        onlineMap = make(map[string]Client)
        msg := <-message // 没有消息前,这里会阻塞
        
        // 遍历 map, 给 map 每个成员发送消息
        for _, cli range onlineMap {
            cli.C <- msg
        }
    }
}

// 给当前客户端发送信息
func WriteMsgToClient(cli Client, conn net.Conn) {
    for msg := range cli.C {
        conn.Write([]byte(msg + "\n"))
    }
}

// 生成 msg
func MakeMsg(cli Client, msg string) (buf string) {
    buf = "[" + cli.Addr + "]" + cli.Name + msg

}

// 处理用户连接
func HandleConn(conn net.Conn) {
    // 获取客户端的网络地址
    cliAddr := conn.RemoteAddr().String()
    
    // 创建一个结构体, 默认,用户名和网络地址一样
    cli := Client{make(chan string), cliAddr, cliAddr}
    
    // 把结构体添加到 map
    onlineMap[cliAddr] = cli
    
    // 新开一个协程,专门给当前客户端发送信息
    go WriteMsgToClient(cli, conn)
    
    // 提示,我是谁
    cli.C <- MakeMsg(cli, "I am here")
    
    // 广播某个人在线
    message <- "[" + cli.Addr + "]" + cli.Name + ": login"
    message <- MakeMsg(cli, "login")
    
    isQuit := make(chan bool) // 对方是否主动退出
    hasData := make(chan bool) // 对方是否有数据发送
    
    // 新建一个协程,接收用户发送过来的数据
    go func() {
        for {
            buf := make([]byte, 2048)
            
            n, err := conn.Read(buf)
            if err != nil {
                // print err
                continue
            }
            
            // 对方断开或者出问题
            if n == 0 {
                isQuit <- true
                // print err          
                return
            }
            
            // 转发此内容
            msg := string(buf[:n])
            
            if len(msg) == 3 && msg == "who" {
                // who 消息,查询在线用户
                conn.Write([]byte("user list:\n"))
                for _, tmp := range onlineMap {
                    msg = tmp.Affr + ":" + tmp.Name + "\n"
                    conn.Write([]byte(msg))
                }
            } else if len(msg) >= 8 && msg[:6] == "rename" {
                // rename 消息
                name = strings.Split(msg, "|")[1]
                cli.Name = name
                onlineMap[cliAddr] = cli
                conn.Write([]byte("rename ok \n"))
            } else {
                // 普通消息,直接转发
                message <- MakeMsg(cli, msg)
            }  
            
            // 表示有数据
            hasData <- true
        }
    }()
    
    for {
        // 通过 select 检测 channel 的流动
        select {
            case <- isQuit:
                // 当前用户从 map 移除
                delete(onlineMap, cliAddr)
                // 广播谁下线了
                message <- MakeMsg(cli, "login out")
                return   
            case <- hasData:
                // 有数据,不处理
            case <-time.After(60 * time.Second):
                // 60s 之后超时处理
                delete(onlineMap, cliAddr)
                // 广播一下谁下线了
                message <- MakeMsg(cli, "time out leave out") 
                return
        }
    }
}

func main() {
    // 监听
    listener, err := net.Listen("tcp", ":8000")
    if err != nil {
        // print err
        return
    }
    
    defer listener.Close()
    
    // 新开一个协程,转发消息,只要有消息来了,就遍历 map,给 map 的每个成员发送消息
    go Manager()
    
    // 主协程,循环阻塞等待用户连接
    for {
        conn, err := listener.Accept()
        if err != nil {
            // print err
            continue
        }
        
        go HandleConn(conn) // 处理用户连接
    }
}
HTTP 编程

1、Web 的工作方式

Web 服务器的工作原理可以简单地归纳为:

  • 客户端通过 TCP/IP 协议建立到服务器的 TCP 连接
  • 客户端向服务器发送 HTTP 协议请求包,请求服务器里的资源文档
  • 服务器向客户端发送 HTTP 协议应答包,如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理 “动态内容”,并将处理得到的数据返回给客户端
  • 客户端与服务器断开。由客户端解释 HTML 文档,在客户端屏幕上渲染图形结果

2、HTTP 协议

HyperText Transfer Protocol, 超文本传输协议,详细规定了浏览器和万维网之间互相通信的规则,通过因特网传送万维网文档的数据传送协议。

HTTP 协议通常承载于 TCP 协议之上,有事也承载于 TLS 或 SSL 协议层之上,这个时候,就成了我们常说的 HTTPS

URL(Unique Resource Location) ,用来表示网络资源,可以理解为网络文件路径

格式:http://host:port/path

3、常见请求方式

GET: 获取资源,不适合上传数据,参数显示在浏览器地址栏,保密性差

POST:用于提交数据,长度没有限制,数据放在请求正文中

4、请求和响应报文格式

请求格式:

#GET/ HTTP/1.1 // 请求行
Host: 127.0.0.1:8000 // 请求头 
Connectin: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0(Windows NT 12.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) 6
Accept: text/html. application/xhtml+xml, application/xml; q=0.9, image/webp, */*; q=0.8
Accept-Encoding:gzip, defalte, sdch, br
Accept-Language:zh-CN, zh; q=0.8
 // 空行
# // 包体

常见请求头:

User-Agent: 请求浏览器类型
Accept: 可是识别响应内容列表
Accept-Language: 可接收自然语言
Accept-Encoding: 可接收的编码压缩格式
Accept-Charset: 可接收的应答的字符集
Host: 请求的主机名,允许多个域名同处一个 IP 地址,即虚拟主机
connection: 连接方式:close / keepalive
Cookie: 存储于客户端拓展字段,向同一域名的服务器发送属于该域名的 cookie

响应报文:

#HTTP/1.1 200 OK // 状态头
Date: SUb, 28 Jan 2018 06:11:59 GMT // 状态行
Content-Length: 12
Content-Type: text/plain; charset=utf-8

hello world

常见状态码:

- 200 OK :客户端请求成功
- 400 Bad Request:请求报文有语法错误
- 401 Unauthorized:未授权访问
- 403 Forbidden: 服务器拒绝服务
- 404 Nof Found: 资源不存在
- 500 Internal Server Error: 服务器内部错误
- 503 Server Unavailable: 服务器临时不能处理客户端请求

http 服务器响应示例:

package main

import (
    "fmt"
    "net/http"
)

// 服务器编写的业务逻辑处理程序
func myHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "hello world")
}

func main() {
    http.HandleFunc("/go", myHandler)
    
    // 在指定的地址进行监听,开启一个 HTTP
    http.LisenAndServe("127.0.0.1:8000", nil)
}

5、HTTP 服务器编程

直接用 HTTP 内置的包实现 HTTP 服务器编程:

package main

import (
    "fmt"
    "net/http"
)

// w, 给客户端回复数据
// r, 读取客户端发送的数据
func HandConn(w http.ResponseWriter, r *Request) {
    // r.URL 请求链接
    // r.Method 请求的方式
    // r.Body 请求体
    // r.RomoteAddr 请求的 IP 地址
    w.Write([]byte("hello go")) // 给客户端回复数据
}

func main() {
    // 注册处理函数,用户联机额,自动调用指定的处理函数
    http.HandleFunc("/", HandConn)
    
    // 监听绑定
    http:ListenAndServe(":8000", nil)
}

6、HTTP 客户端编程

package main

import (
    "fmt"
    "net/http"
)

func main() {
    resp, err := http.Get("http://www.baidu.com")
    if err != nil {
        // print err
        return
    } 
    
    defer resp.Body.Close()
    
    // print resp.Status, resp.StatusCode, resp.Header, resp.Body
    
    // body 需要 io 的方式读取
    var tmp string
    buf := make([]byte, 4 * 1024)
    for {
        n, err := resp.Body.Read(buf)
        if n == 0 {
            // print err
            break
        }
        
        tmp += string
    }
    
    // print tmp 网页内容
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容