文章目录





12. 网络编程

12.1 网络概述

12.1.1 网络协议

从应用的角度出发,协议可理解为“规则”,是数据传输和数据的解释的规则。

假设,A、B双方欲传输文件。规定:
 第一次,传输文件名,接收方接收到文件名,应答OK给传输方;
 第二次,发送文件的尺寸,接收方接收到该数据再次应答一个OK;
 第三次,传输文件内容。同样,接收方接收数据完成后应答OK表示文件内容接收成功。

由此,无论A、B之间传递何种文件,都是通过三次数据传输来完成。A、B之间形成了一个最简单的数据传输规则。双方都按此规则发送、接收数据。A、B之间达成的这个相互遵守的规则即为协议。

这种仅在A、B之间被遵守的协议称之为原始协议。

当此协议被更多的人采用,不断的增加、改进、维护、完善。最终形成一个稳定的、完整的文件传输协议,被广泛应用于各种文件传输过程中。该协议就成为一个标准协议。最早的ftp协议就是由此衍生而来。

12.1.2 分层模型

12.1.2.1 网络分层架构

为了减少协议设计的复杂性,大多数网络模型均采用分层的方式来组织。每一层都有自己的功能,就像建筑物一样,每一层都靠下一层支持。每一层利用下一层提供的服务来为上一层提供服务,本层服务的实现细节对上层屏蔽
【go】网络编程_数据
越下面的层,越靠近硬件;越上面的层,越靠近用户。至于每一层叫什么名字,其实并不重要(面试的时候,面试官可能会问每一层的名字)。只需要知道,互联网分成若干层即可。

  1. 物理层:主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后再转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特。
  2. 数据链路层:定义了如何让格式化数据以帧为单位进行传输,以及如何让控制对物理介质的访问。这一层通常还提供错误检测和纠正,以确保数据的可靠传输。如:串口通信中使用到的115200、8、N、1
  3. 网络层:在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择。Internet的发展使得从世界各站点访问信息的用户数大大增加,而网络层正是管理这种连接的层。
  4. 传输层:定义了一些传输数据的协议和端口号(WWW端口80等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与TCP特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如QQ聊天数据就是通过这种方式传输的)。 主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。常常把这一层数据叫做段。
  5. 会话层:通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或者接受会话请求(设备之间需要互相认识可以是IP也可以是MAC或者是主机名)。
  6. 表示层:可确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。例如,PC程序与另一台计算机进行通信,其中一台计算机使用扩展二一十进制交换码(EBCDIC),而另一台则使用美国信息交换标准码(ASCII)来表示相同的字符。如有必要,表示层会通过使用一种通格式来实现多种数据格式之间的转换。
  7. 应用层:是最靠近用户的OSI层。这一层为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务。

12.1.2.2 层与协议

每一层都是为了完成一种功能,为了实现这些功能,就需要大家都遵守共同的规则。大家都遵守这规则,就叫做“协议”(protocol)。

网络的每一层,都定义了很多协议。这些协议的总称,叫“TCP/IP协议”。TCP/IP协议是一个大家族,不仅仅只有TCP和IP协议,它还包括其它的协议,如下图:
【go】网络编程_服务器_02

12.1.2.3 每层协议的功能

【go】网络编程_golang_03

  1. 链路层
    以太网规定,连入网络的所有设备,都必须具有“网卡”接口。数据包必须是从一块网卡,传送到另一块网卡。通过网卡能够使不同的计算机之间连接,从而完成数据通信等功能。网卡的地址——MAC 地址,就是数据包的物理发送地址和物理接收地址。
  2. 网络层
    网络层的作用是引进一套新的地址,使得我们能够区分不同的计算机是否属于同一个子网络。这套地址就叫做“网络地址”,这是我们平时所说的IP地址。这个IP地址好比我们的手机号码,通过手机号码可以得到用户所在的归属地。

网络地址帮助我们确定计算机所在的子网络,MAC 地址则将数据包送到该子网络中的目标网卡。网络层协议包含的主要信息是源IP和目的IP。

于是,“网络层”出现以后,每台计算机有了两种地址,一种是 MAC 地址,另一种是网络地址两种地址之间没有任何联系,MAC 地址是绑定在网卡上的,网络地址则是管理员分配的,它们只是随机组合在一起。

网络地址帮助我们确定计算机所在的子网络,MAC 地址则将数据包送到该子网络中的目标网卡。因此,从逻辑上可以推断,必定是先处理网络地址,然后再处理 MAC 地址。

  1. 传输层
    当我们一边聊QQ,一边聊微信,当一个数据包从互联网上发来的时候,我们怎么知道,它是来自QQ的内容,还是来自微信的内容?

也就是说,我们还需要一个参数,表示这个数据包到底供哪个程序(进程)使用。这个参数就叫做“端口”(port),它其实是每一个使用网卡的程序的编号。每个数据包都发到主机的特定端口,所以不同的程序就能取到自己所需要的数据。

端口特点:
 对于同一个端口,在不同系统中对应着不同的进程
 对于同一个系统,一个端口只能被一个进程拥有

  1. 应用层
    应用程序收到“传输层”的数据,接下来就要进行解读。由于互联网是开放架构,数据来源五花八门,必须事先规定好格式,否则根本无法解读。“应用层”的作用,就是规定应用程序的数据格式。

12.2 Socket编程

12.2.1 什么是Socket

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

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

12.2.2 TCP的C/S架构

【go】网络编程_数据_04

12.2.3 示例程序

(1)示例代码1,简单版

服务器

package main

import (
"fmt"
"net"
)

func main() {
//监听
listener, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println("err = ", err)
return
}

defer listener.Close()

//阻塞等待用户链接
conn, err := listener.Accept()
if err != nil {
fmt.Println("err = ", err)
return
}

//接收用户的请求
buf := make([]byte, 1024) //1024大小的缓冲区
n, err1 := conn.Read(buf)
if err1 != nil {
fmt.Println("err1 = ", err1)
return
}

fmt.Println("buf = ", string(buf[:n]))

defer conn.Close() //关闭当前用户链接
}

客户端

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?"))

}

(2)示例代码2,并发版

服务器代码

package main

import (
"fmt"
"log"
"net"
"strings"
)

func dealConn(conn net.Conn) {

defer conn.Close() //此函数结束时,关闭连接套接字

//conn.RemoteAddr().String():连接客服端的网络地址
ipAddr := conn.RemoteAddr().String()
fmt.Println(ipAddr, "连接成功")

buf := make([]byte, 1024) //缓冲区,用于接收客户端发送的数据

for {
//阻塞等待用户发送的数据
n, err := conn.Read(buf) //n代码接收数据的长度
if err != nil {
fmt.Println(err)
return
}
//切片截取,只截取有效数据
result := buf[:n]
fmt.Printf("接收到数据来自[%s]==>[%d]:%s\n", ipAddr, n, string(result))
if "exit" == string(result) { //如果对方发送"exit",退出此链接
fmt.Println(ipAddr, "退出连接")
return
}

//把接收到的数据转换为大写,再给客户端发送
conn.Write([]byte(strings.ToUpper(string(result))))
}
}

func main() {
//创建、监听socket
listenner, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
log.Fatal(err) //log.Fatal()会产生panic
}

defer listenner.Close()

for {
conn, err := listenner.Accept() //阻塞等待客户端连接
if err != nil {
log.Println(err)
continue
}

go dealConn(conn)
}
}

客户端代码

package main

import (
"fmt"
"log"
"net"
)

func main() {
//客户端主动连接服务器
conn, err := net.Dial("tcp", "127.0.0.1:8000")
if err != nil {
log.Fatal(err) //log.Fatal()会产生panic
return
}

defer conn.Close() //关闭

buf := make([]byte, 1024) //缓冲区
for {
fmt.Printf("请输入发送的内容:")
fmt.Scan(&buf)
fmt.Printf("发送的内容:%s\n", string(buf))

//发送数据
conn.Write(buf)

//阻塞等待服务器回复的数据
n, err := conn.Read(buf) //n代码接收数据的长度
if err != nil {
fmt.Println(err)
return
}

//切片截取,只截取有效数据
result := buf[:n]
fmt.Printf("接收到数据[%d]:%s\n", n, string(result))
}
}

12.2.3.3 运行结果
【go】网络编程_golang_05
(3)示例代码3,并发版本,与示例代码2几乎相同

客户端代码

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()

go func() {
//从键盘输入内容,给服务器发送内容
str := make([]byte, 1024)
for {
n, err := os.Stdin.Read(str) //从键盘读取内容, 放在str
if err != nil {
fmt.Println("os.Stdin. err = ", err)
return
}

//把输入的内容给服务器发送
conn.Write(str[:n])
}
}()

//接收服务器回复的数据
//切片缓冲
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])) //打印接收到的内容, 转换为字符串再打印
}
}

服务端代码

package main

import (
"fmt"
"net"
"strings"
)

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

//获取客户端的网络地址信息
addr := conn.RemoteAddr().String()
fmt.Println(addr, " conncet sucessful")

buf := make([]byte, 2048)

for {
//读取用户数据
n, err := conn.Read(buf)
if err != nil {
fmt.Println("err = ", err)
return
}
fmt.Printf("[%s]: %s\n", addr, string(buf[:n]))
fmt.Println("len = ", len(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)
}
}

12.2.4 示例代码 文件上传和下载

(1)os.Stat 方法,获取文件属性

package main

import (
"fmt"
"os"
)

func main() {
list := os.Args
if len(list) != 2 {
fmt.Println("useage: xxx file")
return
}

fileName := list[1]

info, err := os.Stat(fileName)
if err != nil {
fmt.Println("err = ", err)
return
}

fmt.Println("name = ", info.Name())
fmt.Println("size = ", info.Size())

}

(2)实现文件发送与接收

文件传输原理
【go】网络编程_数据_06

发送文件

package main

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

//发送文件内容
func SendFile(path string, conn net.Conn) {
//以只读方式打开文件
f, err := os.Open(path)
if err != nil {
fmt.Println("os.Open err = ", err)
return
}

defer f.Close()

buf := make([]byte, 1024*4)

//读文件内容,读多少发多少,一点不差
for {
n, err := f.Read(buf) //从文件读取内容
if err != nil {
if err == io.EOF {
fmt.Println("文件发送完毕")
} else {
fmt.Println("f.Read err = ", err)
}

return
}

//发送内容
conn.Write(buf[:n]) //给服务器发送内容
}

}

func main() {
//提示输入文件
fmt.Println("请输入需要传输的文件:")
var path string
fmt.Scan(&path)

//获取文件名 info.Name()
info, err := os.Stat(path)
if err != nil {
fmt.Println("os.Stat err = ", err)
return
}

//主动连接服务器
conn, err1 := net.Dial("tcp", "127.0.0.1:8000")
if err1 != nil {
fmt.Println("net.Dial err1 = ", err1)
return
}

defer conn.Close()

//给接收方,先发送文件名
_, err = conn.Write([]byte(info.Name()))
if err != nil {
fmt.Println("conn.Write err = ", err)
return
}

//接收对方的回复,如果回复"ok", 说明对方准备好,可以发文件
var n int
buf := make([]byte, 1024)

n, err = conn.Read(buf)
if err != nil {
fmt.Println("conn.Read err = ", err)
return
}

if "ok" == string(buf[:n]) {
//发送文件内容
SendFile(path, conn)
}
}

接收文件

package main

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

//接收文件内容
func RecvFile(fileName string, conn net.Conn) {
//新建文件
f, err := os.Create(fileName)
if err != nil {
fmt.Println("os.Create err = ", err)
return
}

buf := make([]byte, 1024*4)

//接收多少,写多少,一点不差
for {
n, err := conn.Read(buf) //接收对方发过来的文件内容
if err != nil {
if err == io.EOF {
fmt.Println("文件接收完毕")
} else {
fmt.Println("conn.Read err = ", err)
}
return
}

if n == 0 {
fmt.Println("n == 0 文件接收完毕")
break
}

f.Write(buf[:n]) //往文件写入内容
}

}

func main() {
//监听
listenner, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
fmt.Println("net.Listen err = ", err)
return
}

defer listenner.Close()

//阻塞等待用户连接
conn, err1 := listenner.Accept()
if err1 != nil {
fmt.Println("listenner.Accept err = ", err1)
return
}

defer conn.Close()

buf := make([]byte, 1024)
var n int
n, err = conn.Read(buf) //读取对方发送的文件名
if err != nil {
fmt.Println("conn.Read err = ", err)
return
}

fileName := string(buf[:n])

//回复"ok"
conn.Write([]byte("ok"))

//接收文件内容
RecvFile(fileName, conn)

}

12.2.5 示例代码 并发聊天服务器

【go】网络编程_golang_07

package main

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

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

//保存在线用户 cliAddr =====> Client
var onlineMap map[string]Client

var messaage = make(chan string)

//新开一个协程,转发消息,只要有消息来了,遍历map, 给map每个成员都发送此消息
func Manager() {
//给map分配空间,说实话,在这里分配map的空间不太合适
onlineMap = make(map[string]Client)

for {
msg := <-messaage //没有消息前,这里会阻塞

//遍历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"))
}
}

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

return
}

func HandleConn(conn net.Conn) { //处理用户链接
defer conn.Close()

//获取客户端的网络地址
cliAddr := conn.RemoteAddr().String()

//创建一个结构体, 默认,用户名和网络地址一样
cli := Client{make(chan string), cliAddr, cliAddr}
//把结构体添加到map
onlineMap[cliAddr] = cli

//新开一个协程,专门给当前客户端发送信息
go WriteMsgToClient(cli, conn)
//广播某个在线
//messaage <- "[" + cli.Addr + "]" + cli.Name + ": login"
messaage <- MakeMsg(cli, "login")
//提示,我是谁
cli.C <- MakeMsg(cli, "I am here")

isQuit := make(chan bool) //对方是否主动退出
hasData := make(chan bool) //对方是否有数据发送

//新建一个协程,接收用户发送过来的数据
go func() {
buf := make([]byte, 2048)
for {
n, err := conn.Read(buf)
if n == 0 { //对方断开,或者,出问题
isQuit <- true
fmt.Println("conn.Read err = ", err)
return
}

msg := string(buf[:n-1]) //通过windows nc测试,多一个换行
if len(msg) == 3 && msg == "who" {
//遍历map,给当前用户发送所有成员
conn.Write([]byte("user list:\n"))
for _, tmp := range onlineMap {
msg = tmp.Addr + ":" + tmp.Name + "\n"
conn.Write([]byte(msg))
}

} else if len(msg) >= 8 && msg[:6] == "rename" {
// rename|mike
name := strings.Split(msg, "|")[1]
cli.Name = name
onlineMap[cliAddr] = cli
conn.Write([]byte("rename ok\n"))

} else {
//转发此内容
messaage <- MakeMsg(cli, msg)
}

hasData <- true //代表有数据
}

}() //别忘了()

for {
//通过select检测channel的流动
select {
case <-isQuit:
delete(onlineMap, cliAddr) //当前用户从map移除
messaage <- MakeMsg(cli, "login out") //广播谁下线了

return
case <-hasData:

case <-time.After(30 * time.Second): //60s后
delete(onlineMap, cliAddr) //当前用户从map移除
messaage <- MakeMsg(cli, "time out leave out") //广播谁下线了
return
}
}
}

func main() {
//监听
listener, err := net.Listen("tcp", ":8000")
if err != nil {
fmt.Println("net.Listen err = ", err)
return
}

defer listener.Close()

//新开一个协程,转发消息,只要有消息来了,遍历map, 给map每个成员都发送此消息
go Manager()

//主协程,循环阻塞等待用户链接
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener.Accept err = ", err)
continue
}
go HandleConn(conn) //处理用户链接
}
}