1.1 网络编程分类
网络编程有两种:
- TCP socket 编程,是网络编程的主流。之所以叫 Tcp socket 编程,是因为底层是基于 Tcp/ip 协议的. 比如: QQ 聊天
- b/s 结构的 http 编程,我们使用浏览器去访问服务器时,使用的就是 http 协议,而 http 底层依旧是用 tcp socket 实现的。比如: 京东商城
1.2 协议
TCP/IP(Transmission Control Protocol/Internet Protocol)的简写,中文译名为传输控制协议/因特网互联协议,又叫网络通讯协议,这个协议是 Internet 最基本的协议、Internet 国际互联网络的基础,简单地说,就是由网络层的 IP 协议和传输层的 TCP 协议组成的。
1.3 OSI 与 Tcp/ip 参考模型
QQ间相互通讯案例:
1.4 ip地址
每个 internet 上的主机和路由器都有一个 ip 地址,它包括网络号和主机号,ip 地址有 ipv4(32位)或者 ipv6(128 位). 可以通过 ipconfig 来查看
1.5 端口
我们这里所指的端口不是指物理意义上的端口,而是特指 TCP/IP 协议中的端口,是逻辑意义上的端口。
如果把 IP 地址比作一间房子,端口就是出入这间房子的门。真正的房子只有几个门,但是一个 IP 地址的端口 可以有 65536(即:256×256)个之多!端口是通过端口号来标记的,端口号只有整数,范围是从 0 到 65535(256×256-1)
端口分类:
- 0 号是保留端口.
- 1-1024 是固定端口(程序员不要使用)又叫有名端口,即被某些程序固定使用,一般程序员不使用.
22: SSH 远程登录协议 23: telnet 使用 21: ftp 使用
25: smtp 服务使用 80: iis 使用 7: echo 服务 - 1025-65535 是动态端口。这些端口,程序员可以使用.
注意事项:
- 在计算机(尤其是做服务器)要尽可能的少开端口
- 一个端口只能被一个程序监听
- 如果使用 netstat –an 可以查看本机有哪些端口在监听
- 可以使用 netstat –anb 来查看监听端口的 pid,在结合任务管理器关闭不安全的端口
2.1 服务端的处理流程
- 监听端口 8888
- 接收客户端的 tcp 链接,建立客户端和服务器端的链接.
- 创建 goroutine,处理该链接的请求(通常客户端会通过链接发送请求包)
2.2 客户端的处理流程
- 建立与服务端的链接
- 发送请求数据[终端],接收服务器端返回的结果数据
- 关闭链接
2.3 程序示意图
- 服务端监听端口8888,客户端链接8888端口后,建立链接
- 服务端主线程§ 接收到客户端链接时,开启一个协程
- 处理客户端的请求,使得我们可以做一些分支来处理请求
使用go做socket网络开发使用的是 net 包
import "net"
net包提供了可移植的网络I/O接口,包括TCP/IP、UDP、域名解析和Unix域socket。
虽然本包提供了对网络原语的访问,大部分使用者只需要Dial、Listen和Accept函数提供的基本接口;以及相关的Conn和Listener接口。crypto/tls包提供了相同的接口和类似的Dial和Listen函数。
官方示例:
Dial函数和服务端建立连接:
conn, err := net.Dial("tcp", "google.com:80")
if err != nil {
// handle error
}
fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")
status, err := bufio.NewReader(conn).ReadString('\n')
// ...
Listen函数创建的服务端:
ln, err := net.Listen("tcp", ":8080")
if err != nil {
// handle error
}
for {
conn, err := ln.Accept()
if err != nil {
// handle error
continue
}
go handleConnection(conn)
}
四 net包重要类型
4.1 type Dialer
type Dialer struct {
// Timeout是dial操作等待连接建立的最大时长,默认值代表没有超时。
// 如果Deadline字段也被设置了,dial操作也可能更早失败。
// 不管有没有设置超时,操作系统都可能强制执行它的超时设置。
// 例如,TCP(系统)超时一般在3分钟左右。
Timeout time.Duration
// Deadline是一个具体的时间点期限,超过该期限后,dial操作就会失败。
// 如果Timeout字段也被设置了,dial操作也可能更早失败。
// 零值表示没有期限,即遵守操作系统的超时设置。
Deadline time.Time
// LocalAddr是dial一个地址时使用的本地地址。
// 该地址必须是与dial的网络相容的类型。
// 如果为nil,将会自动选择一个本地地址。
LocalAddr Addr
// DualStack允许单次dial操作在网络类型为"tcp",
// 且目的地是一个主机名的DNS记录具有多个地址时,
// 尝试建立多个IPv4和IPv6连接,并返回第一个建立的连接。
DualStack bool
// KeepAlive指定一个活动的网络连接的生命周期;如果为0,会禁止keep-alive。
// 不支持keep-alive的网络连接会忽略本字段。
KeepAlive time.Duration
}
Dialer类型包含与某个地址建立连接时的参数。
每一个字段的零值都等价于没有该字段。因此调用Dialer零值的Dial方法等价于调用Dial函数。
*func (Dialer) Dial
func (d *Dialer) Dial(network, address string) (Conn, error)
Dial在指定的网络上连接指定的地址。参见Dial函数获取网络和地址参数的描述。
4.2 type Listener
type Listener interface {
// Addr返回该接口的网络地址
Addr() Addr
// Accept等待并返回下一个连接到该接口的连接
Accept() (c Conn, err error)
// Close关闭该接口,并使任何阻塞的Accept操作都会不再阻塞并返回错误。
Close() error
}
Listener是一个用于面向流的网络协议的公用的网络监听器接口。多个线程可能会同时调用一个Listener的方法。
func Listen
func Listen(net, laddr string) (Listener, error)
返回在一个本地网络地址laddr上监听的Listener。网络类型参数net必须是面向流的网络:
“tcp”、“tcp4”、“tcp6”、“unix"或"unixpacket”。参见Dial函数获取laddr的语法。
4.3 type 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接口代表通用的面向流的网络连接。多个线程可能会同时调用同一个Conn的方法。
func Dial
func Dial(network, address string) (Conn, error)
在网络network上连接地址address,并返回一个Conn接口。可用的网络类型有:
“tcp”、“tcp4”、“tcp6”、“udp”、“udp4”、“udp6”、“ip”、“ip4”、“ip6”、“unix”、“unixgram”、“unixpacket”
对TCP和UDP网络,地址格式是host:port或[host]:port,参见函数JoinHostPort和SplitHostPort。
Dial("tcp", "12.34.56.78:80")
Dial("tcp", "google.com:http")
Dial("tcp", "[2001:db8::1]:http")
Dial("tcp", "[fe80::1%lo0]:80")
对IP网络,network必须是"ip"、“ip4”、"ip6"后跟冒号和协议号或者协议名,地址必须是IP地址字面值。
Dial("ip4:1", "127.0.0.1")
Dial("ip6:ospf", "::1")
对Unix网络,地址必须是文件系统路径。
func DialTimeout
func DialTimeout(network, address string, timeout time.Duration) (Conn, error)
DialTimeout类似Dial但采用了超时。timeout参数如果必要可包含名称解析。
func Pipe
func Pipe() (Conn, Conn)
Pipe创建一个内存中的同步、全双工网络连接。连接的两端都实现了Conn接口。一端的读取对应另一端的写入,直接将数据在两端之间作拷贝;没有内部缓冲。
4.4 type Addr
type Addr interface {
Network() string // 网络名
String() string // 字符串格式的地址
}
Addr代表一个网络终端地址。
五 实例演示逐步演示客户端向服务端传输信息的完善过程
5.1 编写服务端
思路:
- 根据2.1流程应先创建监听端口
- 根据4.2func Listen函数传入相应值
package main
import (
"fmt"
"net"
)
func main() {
fmt.Println("服务器开始监听")
listen, err := net.Listen("tcp", "0.0.0.0:8888")
//我们指定了协议为tcp,那么其他人来通讯必须也是tcp
//127.0.0.1只支出ipv4访问
//0.0.0.0 支持ipv4和ipv6
if err != nil {
//根据返回值判断监听是否成功
fmt.Println("listen err=", err)
return
}
fmt.Println(listen)
}
在终端启动程序发现跑一下就停止了:
PS C:\Users\Mechrevo\Desktop\go_pro\server> go run server.go
服务器开始监听
&{0xc00007aa00 {<nil> 0}}
PS C:\Users\Mechrevo\Desktop\go_pro\server>
Accept()
package main
import (
"fmt"
"net"
)
func main() {
fmt.Println("服务器开始监听")
listen, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println("listen err=", err)
return
}
defer listen.Close()
//Listen 函数返回的是Listener结构体
//该结构体下有一个Close的关闭连接的方法
//这里当请求接收后就断掉请求
for {
//创建for循环让程序不要直接退出
fmt.Println("等待客户端链接")
conn, err := listen.Accept() //使用Accept方法,等待用户连接,如果没有请求就一直卡在这里
//当请求结束后,进入下一个循环再次等待
if err != nil {
fmt.Println("Accept() err", err) //如果连接失败了我们为了不影响其他线程
//不使用return退出for循环
} else {
fmt.Println("链接成功了", conn)
}
}
}
测试:
PS C:\Users\Mechrevo\Desktop\go_pro\server> go run server.go
服务器开始监听
等待客户端链接
telnet 127.0.0.1 8888
PS C:\Users\Mechrevo\Desktop\go_pro\server> go run server.go
服务器开始监听
等待客户端链接
链接成功了 &{{0xc00007ac80}}
等待客户端链接
可以看到已经链接请求成功了,并且因为for循环还会卡在Accept处等待请求
5.2 编写客户端
根据4.1中func (*Dialer) Dial函数传入相应值与sever端建立连接
package main
import (
"fmt"
"net"
)
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8888")
//连服务端的协议必须和服务端一致
if err != nil {
fmt.Println("链接失败")
return
}
fmt.Println("链接服务端成功", conn)
}
运行结果:
client端:
server端:
5.3 服务端获取客户端信息
上述过程中Dial 函数返回的是一个Conn的接口,客户端连接后,客户端和服务端都会得到一个相同的Conn 接口
在Conn这个请求连接的结构体中,有一个RemoteAddr() 的方法,他主要的作用就是获取整个连接下的一个读写权限
修改之后的server端为:
package main
import (
"fmt"
"net"
)
func main() {
fmt.Println("服务器开始监听")
listen, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println("listen err=", err)
return
}
defer listen.Close()
for {
fmt.Println("等待客户端链接")
conn, err := listen.Accept()
if err != nil {
fmt.Println("Accept() err", err)
} else {
fmt.Printf("链接成功了=%v ipadd=%v\n", conn, conn.RemoteAddr().String())
//RemoteAddr() 客户端信息,string转字符串
fmt.Println(conn.RemoteAddr().Network())
}
}
}
测试结果:
这样就拿到了客户端的ip以及端口
5.4 客户端提交数据给服务端
通过bufio.NewReader 方法去建立缓存,并指定os.Stdin去读取标准输入
func Input() string {
reader := bufio.NewReader(os.Stdin) //os.stdin 标准输入,接收用户终端信息
line,err := reader.ReadString('\n') //读取标准输入数据,截止到\n 换行结束
if err != nil {
fmt.Println("readString err=",err)
}
return line
}
此时又需要将用户输入的数据发送给服务端,又将用到Conn接口
可以把Conn看做是一个传输带,通过Write可以将输入进行写入
然后再通过Read去读取写入的数据,这里我们读取写入的类型是一个切片
返回值中有个int和error,这里的int是指切片的长度,方便我们取出切片所有数据
修改之后的client端为:
package main
import(
"bufio"
"fmt"
"net"
"os"
)
func Input() string {
reader := bufio.NewReader(os.Stdin) //os.stdin 标准输入,接收用户终端信息
line,err := reader.ReadString('\n') //读取标准输入数据,截止到\n 换行结束
if err != nil {
fmt.Println("readString err=",err)
}
return line
}
func main(){
conn,err := net.Dial("tcp","127.0.0.1:8888")
if err != nil{
fmt.Println("链接失败")
return
}
line := Input()
n,err := conn.Write([]byte(line)) //返回切片的总长度
if err != nil{
fmt.Println("conn.Write err=",err)
}
fmt.Printf("客户端发送了 %d 字节的数据,并退出",n)
}
测试:
5.5 服务端接收客户端数据
在5.4中我们已经实现客户端提交数据的功能,但此时服务端需要有接受数据的功能
可以使用conn接口中的Read去读取客户端写入的数据
修改之后的server端:
package main
import (
"fmt"
"net"
)
func process(conn net.Conn) {
//处理客户端请求函数
defer conn.Close()
//表示当前传进来的这个客户端连接只要下面逻辑处理完了,那么就直接结束掉这个连接,如果还要处理就再次连接
for {
buf := make([]byte, 1024)
//客户端write写入的数据是一个 []byte类型的数据
//但是我们不清楚他具体要写多大的数据,所以这里空间大小先设置为1024
fmt.Println("服务器在等待客户端发送信息\n", conn.RemoteAddr().String())
n, err := conn.Read(buf)
//通过Read方法,去接收客户端发送的数据
//这里指定了buf的切片, 意思是把客户端发送的数据存放到buf变量中
//Read会等待客户端通过conn发送信息
//如果客户端没有发送write动作,那么这个协程就会阻塞在这里等待
if err != nil {
fmt.Println("服务器端的read err=", err)
return
//当客户端完成任务或异常关闭后,这边我们就将协程退出,否则会循环报错链接
}
fmt.Print(string(buf[:n])) //打印输出
//这里的Print不用带ln,因为过来的数据是带换行的
}
}
func main() {
fmt.Println("服务器开始监听了")
listen, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println("listen err=", err)
return
}
defer listen.Close()
for {
fmt.Println("等待客户端链接")
conn, err := listen.Accept()
if err != nil {
fmt.Println("Accept() err", err)
} else {
fmt.Printf("链接成功了=%v ipadd=%v", conn, conn.RemoteAddr().String())
go process(conn)
//当客户端连接成功了,我们开启一个协程去处理这个连接
}
}
}
测试:
client端:
server端:
可以看到,在服务端接收并输出第一次数据后,会再次等待客户端输入。但是客户端已经退出了,服务端发现客户端断线,这个协程就也退出了
5.6 客户端循环发送与退出功能
上述代码中还存在客户端每次链接只能传输一次数据,不太人性化,因此我们需要实现能循环发送的功能,并且每次传输完成后程序自动退出断开连接,因此需添加一个功能,能让用户输入exit时才退出程序并断开连接。
对 client.go 做了改进:
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
func Input(conn net.Conn) string {
reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString('\n')
if err != nil {
fmt.Println("readString err=", err)
}
line = strings.Trim(line, "\r\n")
//我们客户端输入完成后会按回车键
//这样会携带一个\n
//有时候我们不想要这个\n 可以这么去除
if line == "exit" {
//如果用户输入的是exit就退出
return "exit"
}
_, err = conn.Write([]byte(line + "\n"))
//上面我们已经去除了\n 这里为了显示好看再加上,或者去服务端加也行
if err != nil {
fmt.Println("conn.Write err=", err)
}
return line
}
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println("链接失败")
return
}
for {
line := Input(conn)
if line == "exit" { //如果用户输入的是exit就退出
fmt.Println("客户端退出")
break
}
}
}
测试:
client端:
server端:
5.7 实例演示完整代码
服务端:
package main
import (
"fmt"
"net"
)
func process(conn net.Conn) {
defer conn.Close()
for {
buf := make([]byte, 1024)
fmt.Println("服务器在等待客户端发送信息\n", conn.RemoteAddr().String())
n, err := conn.Read(buf)
if err != nil {
fmt.Println("服务器端的read err=", err)
return
}
fmt.Print(string(buf[:n]))
}
}
func main() {
fmt.Println("服务器开始监听")
listen, err := net.Listen("tcp", "0.0.0.0:8888")
if err != nil {
fmt.Println("listen err=", err)
return
}
defer listen.Close()
for {
fmt.Println("等待客户端链接")
conn, err := listen.Accept()
if err != nil {
fmt.Println("Accept() err", err)
} else {
fmt.Printf("链接成功了=%v ipadd=%v", conn, conn.RemoteAddr().String())
go process(conn)
}
}
}
客户端:
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
func Input(conn net.Conn) string {
reader := bufio.NewReader(os.Stdin)
line, err := reader.ReadString('\n')
if err != nil {
fmt.Println("readString err=", err)
}
line = strings.Trim(line, "\r\n")
if line == "exit" {
return "exit"
}
_, err = conn.Write([]byte(line + "\n"))
if err != nil {
fmt.Println("conn.Write err=", err)
}
return line
}
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println("链接失败")
return
}
for {
line := Input(conn)
if line == "exit" {
fmt.Println("客户端退出")
break
}
}
}