golang学习十:网络编程
Posted 浅弋、璃鱼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang学习十:网络编程相关的知识,希望对你有一定的参考价值。
文章目录
一、网络协议:
1. 典型协议:
- 传输层: 常见协议有TCP/UDP协议;
- 应用层: 常见的协议有HTTP协议,FTP协议;
- 网络层: 常见协议有IP协议、ICMP协议、IGMP协议;
- 网络接口层: 常见协议有ARP协议、RARP协议;
- TCP传输控制协议(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议;
- UDP用户数据报协议(User Datagram Protocol)是OSI参考模型中一种无连接的传输层协议, 提供面向事务的简单不可靠信息传送服务;
- HTTP超文本传输协议(Hyper Text Transfer Protocol)是互联网上应用最为广泛的一种网络协议;
- FTP文件传输协议(File Transfer Protocol)
- IP协议是因特网互联协议(Internet Protocol)
- ICMP协议是Internet控制报文协议(Internet Control Message Protocol): 它是TCP/IP协议族的一个子协议, 用于在IP主机、路由器之间传递控制消息;
- IGMP协议是 Internet 组管理协议(Internet Group Management Protocol), 是因特网协议家族中的一个组播协议; 该协议运行在主机和组播路由器之间;
- ARP协议是正向地址解析协议(Address Resolution Protocol) 通过已知的IP, 寻找对应主机的MAC地址;
- RARP是反向地址转换协议, 通过MAC地址确定IP地址;
2.分层模型:
2.1 网络分层架构:
为了减少协议设计的复杂性, 大多数网络模型均采用分层的方式来组织; 每一层都有自己的功能, 就像建筑物一样, 每一层都靠下一层支持; 每一层利用下一层提供的服务来为上一层提供服务, 本层服务的实现细节对上层屏蔽;
业内普遍的分层方式有两种: OSI七层模型 和 TCP/IP四层模型, 可以通过背诵两个口诀来快速记忆:
-
OSI七层模型: 物、数、网、传、会、表、应;
-
TCP/IP四层模型: 链、网、传、应;
-
物理层: 主要定义物理设备标准, 如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等; 它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输, 到达目的地后再转化为1、0, 也就是我们常说的数模转换与模数转换); 这一层的数据叫做比特;
-
数据链路层: 定义了如何让格式化数据以帧为单位进行传输, 以及如何让控制对物理介质的访问; 这一层通常还提供错误检测和纠正, 以确保数据的可靠传输; 如:串口通信中使用到的115200、8、N、1;
-
网络层: 在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择; Internet的发展使得从世界各站点访问信息的用户数大大增加, 而网络层正是管理这种连接的层;
-
传输层: 定义了一些传输数据的协议和端口号(WWW端口80等), 如: TCP(传输控制协议, 传输效率低, 可靠性强, 用于传输可靠性要求高, 数据量大的数据); UDP(用户数据报协议, 与TCP特性恰恰相反, 用于传输可靠性要求不高, 数据量小的数据, 如QQ聊天数据就是通过这种方式传输的); 主要是将从下层接收的数据进行分段和传输, 到达目的地址后再进行重组; 常常把这一层数据叫做段;
-
会话层: 通过传输层(端口号: 传输端口与接收端口)建立数据传输的通路; 主要在系统之间发起会话或者接受会话请求(设备之间需要互相认识可以是IP也可以是MAC地址或者是主机名)。
-
表示层: 可确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取; 例如: PC程序与另一台计算机进行通信,其中一台计算机使用扩展二一十进制交换码(EBCDIC),而另一台则使用美国信息交换标准码(ASCII)来表示相同的字符; 如有必要, 表示层会通过使用一种通用格式来实现多种数据格式之间的转换;
-
应用层: 是最靠近用户的OSI层; 这一层为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务;
2.2 层与协议:
每一层都是为了完成一种功能, 为了实现这些功能, 就需要大家都遵守共同的规则; 大家都遵守这规则, 就叫做协议(protocol);
网络的每一层, 都定义了很多协议; 这些协议的总称, 叫TCP/IP协议; TCP/IP协议是一个大家族, 不仅仅只有TCP和IP协议, 它还包括其它的协议, 如下图:
协议功能:
- 链路层:
- 以太网规定, 连入网络的所有设备, 都必须具有网卡接口; 数据包必须是从一块网卡, 传送到另一块网卡; 通过网卡能够使不同的计算机之间连接, 从而完成数据通信等功能; 网卡的地址 —— MAC 地址, 就是数据包的物理发送地址和物理接收地址;
- 网络层:
- 网络层的作用是引进一套新的地址, 使得能够区分不同的计算机是否属于同一个子网络; 这套地址就叫做网络地址(IP地址);
- 网络地址帮助我们确定计算机所在的子网络, MAC 地址则将数据包送到该子网络中的目标网卡; 网络层协议包含的主要信息是源IP和目的IP;
- 于是, 网络层出现以后, 每台计算机有了两种地址: 一种是 MAC 地址, 另一种是IP地址; 两种地址之间没有任何联系, MAC 地址是绑定在网卡上的,网络地址则是管理员分配的,它们只是随机组合在一起。
- 网络地址帮助我们确定计算机所在的子网络,MAC 地址则将数据包送到该子网络中的目标网卡。因此,从逻辑上可以推断,必定是先处理网络地址,然后再处理 MAC 地址。
- 传输层:
- 当我们一边聊QQ,一边聊微信,当一个数据包从互联网上发来的时候,我们怎么知道,它是来自QQ的内容,还是来自微信的内容?
- 也就是说,我们还需要一个参数,表示这个数据包到底供哪个程序(进程)使用。这个参数就叫做“端口”(port),它其实是每一个使用网卡的程序的编号。每个数据包都发到主机的特定端口,所以不同的程序就能取到自己所需要的数据。
- 端口特点:
- 对于同一个端口,在不同系统中对应着不同的进程
- 对于同一个系统,一个端口只能被一个进程拥有
- 应用层:
- 应用程序收到“传输层”的数据,接下来就要进行解读。由于互联网是开放架构,数据来源五花八门,必须事先规定好格式,否则根本无法解读。“应用层”的作用,就是规定应用程序的数据格式
2.3 通信过程
两台计算机通过TCP/IP协议通讯的过程如下所示:
2.4 总结通信过程:
- mac地址(不需要用户指定) --> (ARP 协议)Ip ——> mac
- IP 地址(需要用户指定) ——> 确定主机
- port 端口号(需要用户指定) ——> 确定程序
- 不能使用系统占用的默认端口; 建议使用5000+ 端口;
- 65535为端口上限;
二、Socket编程:
1.什么是Socket:
Socket, 英文含义是插座、插孔, 一般称之为套接字, 用于描述IP地址和端口; 可以实现不同程序间的数据通信;
Socket起源于Unix, 而Unix基本哲学之一就是“一切皆文件”, 都可以用“打开open –> 读写write/read –> 关闭close”模式来操作;
Socket就是该模式的一个实现, 网络的Socket数据传输是一种特殊的I/O, Socket也是一种文件描述符; Socket也具有一个类似于打开文件的函数调用: Socket()
, 该函数返回一个整型的Socket描述符, 随后的连接建立、数据传输等操作都是通过该Socket实现的;
在TCP/IP协议中, "IP地址+TCP或UDP端口号"唯一标识网络通讯中的一个进程; "IP地址+端口号"就对应一个socket; 欲建立连接的两个进程各自有一个socket来标识, 那么这两个socket组成的socket pair就唯一标识一个连接; 因此可以用Socket来描述网络连接的一对一关系;
- 常用的Socket类型有两种: 流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM);
- 流式是一种面向连接的Socket, 针对于面向连接的TCP服务应用;
- 数据报式Socket是一种无连接的Socket, 对应于无连接的UDP服务应用;
2. 网络应用程序设计模式:
2.1 模式:
-
C/S模式:
- 传统的网络应用设计模式, 客户机(client)/服务器(server)模式; 需要在通讯两端各自部署客户机和服务器来完成数据通信;
-
B/S模式:
- 浏览器(Browser)/服务器(Server)模式; 只需在一端部署服务器, 而另外一端使用每台PC都默认配置的浏览器即可完成数据的传输;
-
优缺点:
- 对于C/S模式来说, 其优点明显;
- 客户端位于目标主机上可以保证性能, 将数据缓存至客户端本地, 从而提高数据传输效率;
- 一般来说客户端和服务器程序由一个开发团队创作, 所以他们之间所采用的协议相对灵活;
- 可以在标准协议的基础上根据需求裁剪及定制; 例如, 腾讯所采用的通信协议,即为ftp协议的修改剪裁版;
- 因此, 传统的网络应用程序及较大型的网络应用程序都首选C/S模式进行开发; 如, 知名的网络游戏魔兽世界; 3D画面, 数据量庞大, 使用C/S模式可以提前在本地进行大量数据的缓存处理, 从而提高观感;
- C/S模式的缺点也较突出:
- 由于客户端和服务器都需要有一个开发团队来完成开发;
- 工作量将成倍提升, 开发周期较长;
- 从用户角度出发, 需要将客户端安插至用户主机上, 对用户主机的安全性构成威胁;
- 这也是很多用户不愿使用C/S模式应用程序的重要原因;
- B/S模式相比C/S模式而言, 由于它没有独立的客户端, 使用标准浏览器作为客户端, 其工作开发量较小; 只需开发服务器端即可; 另外由于其采用浏览器显示数据, 因此移植性非常好, 不受平台限制; 如早期的偷菜游戏, 在各个平台上都可以完美运行;
- B/S模式的缺点也较明显:
- 由于使用第三方浏览器, 因此网络应用支持受限;
- 另外, 没有客户端放到对方主机上, 缓存数据不尽如人意, 从而传输数据量受到限制; 应用的观感大打折扣;
- 第三, 必须与浏览器一样, 采用标准http协议进行通信, 协议选择不灵活;
- 因此在开发过程中, 模式的选择由上述各自的特点决定; 根据实际需求选择应用程序设计模式;
- 对于C/S模式来说, 其优点明显;
2.2 TCP的C/S架构:
2.3 简单的C/S模型通信:
a.Server端:
Listen函数:
func Listen(network, address string) (Listener, error)
- network: 选用的协议: TCP、UDP, 如:“tcp”或 “udp”;
- address: IP地址+端口号,如: “127.0.0.1:8000”或 “:8000”;
Listener 接口:
type Listener interface
Accept() (Conn, error)
Close() error
Addr() Addr
Conn 接口:
type Conn interface
Read(b []byte) (n int, err error)
Write(b []byte) (n int, err error)
Close() error
LocalAddr() Addr
RemoteAddr() Addr
SetDeadline(t time.Time) error
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error
参看 https://studygolang.com/pkgdoc 中文帮助文档中的demo:
- TCP-CS服务器:
- 创建监听
socket
:listener := net.Listen("TCP", "IP+port")
=> IP+port --服务器自己的IP 和 port; - 启动监听
conn := listener.Accept()
:conn
用于 通信的socket
; conn.Read()
;- 处理使用 数据;
- 返回数据:
conn.Write()
; - 关闭
listener
、conn
;
- 创建监听
TCP_Server.go
package main
import (
"fmt"
"net"
)
func main()
// 指定服务器
listener, err := net.Listen("tcp", "0.0.0.0:9000")
if err != nil
fmt.Println("net.Listen err: ", err)
return
defer listener.Close()
fmt.Println("服务器等待客户端建立连接...")
// 阻塞监听客户端请求
conn, err := listener.Accept()
if err != nil
fmt.Println("listener.Accept err: ", err)
return
defer conn.Close()
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil
fmt.Println("conn.Read err: ", err)
return
// 打印数据
fmt.Println("服务器读取到: ", string(buf[:n]))
// 回复数据
if _, err := conn.Write([]byte("server is ok")); err != nil
fmt.Println("conn.Write err: ", err)
在整个通信过程中, 服务器端有两个socket参与进来, 但用于通信的只有 conn 这个socket; 它是由 listener创建的; 隶属于服务器端
b.Client 端:
Dial函数:
func Dial(network, address string) (Conn, error)
- network: 选用的协议:TCP、UDP, 如:“tcp”或 “udp”;
- address: 服务器IP地址+端口号, 如: “121.36.108.11:8000”或 “www.itcast.cn:8000”;
Conn 接口:
type Conn interface
Read(b []byte) (n int, err error)
Write(b []byte) (n int, err error)
Close() error
LocalAddr() Addr
RemoteAddr() Addr
SetDeadline(t time.Time) error
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error
- TCP-CS客户端:
conn, err := net.Dial("TCP", 服务器的IP+port)
;- 写数据给 服务器
conn.Write()
- 读取服务器回发的 数据
conn.Read()
conn.Close()
TCP_Client.go
package main
import (
"fmt"
"net"
)
func main()
// 主动连接请求
conn, err := net.Dial("tcp", "127.0.0.1:9000")
if err != nil
fmt.Println("net.Dial err: ", err)
return
defer conn.Close()
// 发送数据
_, err = conn.Write([]byte("R U ok??"))
if err != nil
fmt.Println("onn.Write err: ", err)
return
// 接收数据
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil
fmt.Println("conn.Read err: ", err)
return
// 打印数据
fmt.Println("客户端读取到: ", string(buf[:n]))
2.4 并发的C/S模型通信:
a.并发server:
现在已经完成了客户端与服务端的通信, 但是服务端只能接收一个用户发送过来的数据, 怎样接收多个客户端发送过来的数据, 实现一个高效的并发服务器呢?
Accept()
函数的作用是等待客户端的链接, 如果客户端没有链接, 该方法会阻塞; 如果有客户端链接, 那么该方法返回一个Socket负责与客户端进行通信; 所以, 每来一个客户端, 该方法就应该返回一个Socket与其通信, 因此, 可以使用一个死循环, 将Accept()
调用过程包裹起来;
ps:
-
实现并发处理多个客户端数据的服务器, 就需要针对每一个客户端连接, 单独产生一个Socket, 并创建一个单独的goroutine与之完成通信;
-
TCP-CS并发服务器:
- 创建 监听套接字
listener := net.Listen("tcp", 服务器的IP+port)
defer listener.Close()
for
循环 阻塞监听 客户端连接事件:conn := listener.Accept()
- 创建 go程 对应每一个 客户端进行数据通信
go HandlerConnet()
- 实现
HandlerConnet(conn net.Conn)
defer conn.Close()
- 获取成功连接的客户端
Addr
:conn.RemoteAddr()
- for 循环 读取 客户端发送数据:
conn.Read(buf)
- 处理数据 小 -> 大
strings.ToUpper()
- 回写转化后的数据:
conn.Write(buf[:n])
- 创建 监听套接字
并发Service.go
package main
import (
"fmt"
"io"
"net"
"strings"
)
func Handler(conn net.Conn)
defer conn.Close()
// 获取客户端的网络地址
addr := conn.RemoteAddr().String()
fmt.Println(addr, "connect successful!!!")
buf := make([]byte, 4096)
for
n, err := conn.Read(buf)
if err != nil
if err == io.EOF
fmt.Println("client close!!!")
return
fmt.Println("conn.Read err: ", err)
return
// 打印数据
fmt.Println("服务器读取到: ", string(buf[:n]))
// 回复数据
if _, err := conn.Write([]byte(strings.ToUpper(string(buf[:n])))); err != nil
fmt.Println("conn.Write err: ", err)
func main()
// 指定服务器
listener, err := net.Listen("tcp", "0.0.0.0:9000")
if err != nil
fmt.Println("net.Listen err: ", err)
return
defer listener.Close()
fmt.Println("服务器等待客户端建立连接...")
for
// 阻塞监听客户端请求
conn, err := listener.Accept()
if err != nil
fmt.Println("listener.Accept err: ", err)
return
go Handler(conn)
b.并发Client
客户端不仅需要持续的向服务端发送数据, 同时也要接收从服务端返回的数据; 因此可将发送和接收放到不同的协程中;
主协程循环接收服务器回发的数据(该数据应已转换为大写), 并打印至屏幕; 子协程循环从键盘读取用户输入数据, 写给服务器; 读取键盘输入可使用 os.Stdin.Read(str)
; 定义切片str, 将读到的数据保存至str中;
package main
import (
"fmt"
"net"
"os"
)
func main()
// 主动连接请求
conn, err := net.Dial("tcp", "127.0.0.1:9000")
if err != nil
fmt.Println("net.Dial err: ", err)
return
defer conn.Close()
go func()
str := make([]byte, 1024)
for
n, err := os.Stdin.Read(str)
if err != nil
fmt.Println("os.Stdin.Read err:", err)
return
// 将从键盘读到的数据,发送给服务器
_, err = conn.Write(str[:n]) // 读多少,写多少
if err != nil
fmt.Println("conn.Write err:", err)
return
()
// 主协程, 接收服务器数据
buf := make([]byte, 4096)
for
n, err := conn.Read(buf)
if err != nil
fmt.Println("conn.Read err: ", err)
return
// 打印数据
fmt.Println("客户端读取到: ", string(buf[:n]))
3. TCP通信
下图是一次TCP通讯的时序图; TCP连接建立断开; 包含熟知的三次握手和四次握手:
3.1 三次握手:
…
3.2 四次挥手:
…
4. UDP:
4.1 UDP服务器
由于UDP是无连接的, 所以, 服务器端不需要额外创建监听套接字, 只需要指定好IP和port, 然后监听该地址, 等待客户端与之建立连接, 即可通信;
- 创建监听地址:
func ResolveUDPAddr(network, address string) (*UDPAddr, error)
;
- 创建监听连接:
func ListenUDP(network string, laddr *UDPAddr) (*UDPConn, error)
;
- 接收udp数据:
func (c *UDPConn) ReadFromUDP(b []byte) (int, *UDPAddr, error)
;
- 写出数据到udp:
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)
;
udp_server.go
package main
import (
"fmt"
"net"
)
func main()
//创建监听的地址,并且指定udp协议
udp_addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8002")
if err != nil
fmt.Println("ResolveUDPAddr err:", err)
return
conn, err := net.ListenUDP("udp", udp_addr) //创建监听链接
if err != nil
fmt.Println("ListenUDP err:", err)
return
defer conn.Close()
buf := make([]byte, 1024)
n, raddr, err := conn.ReadFromUDP(buf) //接收客户端发送过来的数据,填充到切片buf中。
if err != nil
return
fmt.Println("客户端发送:", string(buf[:n]))
_, err = conn.WriteToUDP([]byte("nice to see u in udp"), raddr) //向客户端发送数据
if err != nil
fmt.Println("WriteToUDP err:", err)
return
4.2 UDP客户端:
UDP_client.go
package main
import (
"net"
"fmt"
)
func main()
conn, err := net.Dial("udp", "127.0.0.1:8002")
if err != nil
fmt.Println("net.Dial err:", err)
return
defer conn.Close()
conn.Write([]byte("Hello! I'm client in UDP!"))
buf := make([]byte, 1024)
n, err1 := conn.Read(buf)
if err1 != nil
return
fmt.Println("服务器发来:", string(buf[:n]))
4.3 UDP并发:
UDP_async_server.go:
package main
import (
"net"
"fmt"
)
func main()
// 创建 服务器 UDP 地址结构。指定 IP + port
laddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8003")
if err != nil
fmt.Println("ResolveUDPAddr err:", err)
return
// 监听 客户端连接
conn, err := net.ListenUDP("udp", laddr)
if err != nil
fmt.Println("net.ListenUDP err:", err)
return
以上是关于golang学习十:网络编程的主要内容,如果未能解决你的问题,请参考以下文章
张季跃 201771010139《面向对象程序设计(java)》第十三周学习总结