一 基础知识

1.1 网络编程分类

网络编程有两种:

  1. TCP socket 编程,是网络编程的主流。之所以叫 Tcp socket 编程,是因为底层是基于 Tcp/ip 协议的. 比如: QQ 聊天
  2. 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 是动态端口。这些端口,程序员可以使用.

注意事项:

  1. 在计算机(尤其是做服务器)要尽可能的少开端口
  2. 一个端口只能被一个程序监听
  3. 如果使用 netstat –an 可以查看本机有哪些端口在监听
  4. 可以使用 netstat –anb 来查看监听端口的 pid,在结合任务管理器关闭不安全的端口
二 tcp socket 编程流程

2.1 服务端的处理流程

  1. 监听端口 8888
  2. 接收客户端的 tcp 链接,建立客户端和服务器端的链接.
  3. 创建 goroutine,处理该链接的请求(通常客户端会通过链接发送请求包)

2.2 客户端的处理流程

  1. 建立与服务端的链接
  2. 发送请求数据[终端],接收服务器端返回的结果数据
  3. 关闭链接

2.3 程序示意图

在这里插入图片描述

  1. 服务端监听端口8888,客户端链接8888端口后,建立链接
  2. 服务端主线程§ 接收到客户端链接时,开启一个协程
  3. 处理客户端的请求,使得我们可以做一些分支来处理请求
三 net包

使用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 编写服务端

思路:

  1. 根据2.1流程应先创建监听端口
  2. 根据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
		}
	}

}