网络编程有两种:
socket可以通过网络连接让多个进程建立通信并且相互传输数据,不管进程是不是在一个主机上。这意味这socket可以用来提供网络中不同计算机的多个应用程序的通信,可以用于单个计算机上的多个应用程序之间通信。不过,大部分都是在网络中通信【网络通信是基于TCP/IP协议】
操作系统内核提供了socket接口。在linux上,存在一个名为socket的系统调用,可以创建一个socket实例
/*
* 参数:
* domain: 通信域
* type:类型
* protocal:通信范围
* 返回值:
* 如果正确,返回一个int类型的值,表示该socket实例唯一标识符的文件描述符。这个值可以调用其他系统调用来进行各种操作,比如绑定和监听端口、发送数据等
*/
int socket(int domain, int type, int protocol)
每隔socket都必将存在一个通信域中,而通信域决定了该socket的地址格式和范围:
由上图可知:Linux提供的通信socket域由三个,分别代表了IPv4域、IPv6域、Unix域。这三个域的标识符都以AF_(address family)为前缀,表示地址族,这也暗示了每个域的socket的地址格式不同。
有很多不同的socket,这些socket相关的特性如下图:
由上表也可以看出,数据形式有两种:数字报和字节流
面向连接与无连接socket:
在面向有链接的socket之间传输数据之前,我们必须先建立逻辑来凝结,在连接建立之后,通信双方可以很方便的互相传递数据,并且由于连接已经暗含了对方的地址,所以在传输数据的时候不必在指定目标地址。【跟谁建立的就和谁通信,这个连接不能在和别人通信了】
面向无连接的socket则不同,这类socket在通信的时候不需建立连接,它们传输的每一个数据包都是独立的,并且会直接发送到网络上,这些数据包中都含有目标地址,因此每个数据包都可能发送到不同的目的地。另外,在面向无连接的socket上,数据流只能是单向的,要么只能发送,要么只能接收。
数据传输的有序性和可靠性与socket是否面向连接有很大的关系,正是因为逻辑连接的存在,通信双方才可以通过一些手段(比如基于TCP协议的序列号和确认应答)来保证从数据发送方的数据能够及时有效正确有序的到达数据接收方,并且被接收方接受。
ps:SOCK_RAW类型的socke提供了一个可以直接通过底层(TCP/IP协议中的网络互连层)传输数据的方法。为了保证安全性,应用程序必须具有操作系统的超级用户权限才能使用这种方式。并且,应用程序需要自己建立数据传输格式【类似TCP协议的数据段格式、UDP的数据报格式】。因此,这个类型的socket很少用。
在调用socket的时候,第三个参数一般是0,表示让操作系统内核根据第一个参数和第二个1参数自己决定socket所用的协议。也就是说,socket的通信域和使用协议之间穿在对应关系,如下图:
注:
socket接口与TCP/IP协议栈与操作系统内核的关系:
基于TCP/IP协议的socket通信的一个简单流程:
/*
* 作用:获取监听器
* 参数:
* * network:以何种协议监听给定的地址,必须是面向流的协议
* * address: 比如127.0.0.1:8080
* 返回值:
* * Listener:表示监听器
* * error:错误信息
*/
func Listen(network, address string) (Listener, error)
listen, err := net.Listen("tcp", ":8888")
第2个API:等待客户端连接
// 当调用监听器的Accept()方法时,流程会被阻塞,直到某个客户端程序与应用程序建立TCP连接。
/*
* 返回值: conn,表示当前TCP连接的net.Conn类型值
*/
conn, err := listen.Accept() // 创建用户数据通信的socket
第3个API:这是一个客户端API
/*
* 作用:向指定的网络地址发送连接建立申请
* 参数:
* * network:类似第一个API,只是取值更加广泛(因为在发送数据之前不一定要先建立连接)
* * address:网络地址,类似 192.168.1.11:8888
* 返回值:
* * Conn
* * err:
*/
func Dial(network, address string) (Conn, error)
conn, err := net.Dial("tcp", "127.0.0.1:8888")
第4个API:这是一个客户端API
/*
* 作用:向指定的网络地址发送连接建立申请
* 参数:
* * network:类似第一个API,只是取值更加广泛(因为在发送数据之前不一定要先建立连接)
* * address:网络地址,类似 192.168.1.11:8888
* * timeout超时时间:单位是纳秒
* 返回值:
* * Conn
* * err:
*/
func DialTimeout(network, address string, timeout time.Duration) (Conn, error)
调用示例:
net.DialTimeout("tcp", "127.0.0.1:8888", 2 * time.Second)
上面的4个API已经足够在服务端程序和客户端程序之间建立TCP连接了【里面封装了一些底层操作,比如创建socket实例、绑定地址机地址等】
在创建监听器并且开始等待连接请求之后,一旦收到客户端的连接请求,服务端就会与客户端建立TCP连接(三次握手)。这个连接的建立过程是由两端的操作系统内核共同协调完成的。当成功建立连接之后,不管是服务器还是客户端,都会得到一个net.Conn类型的值,此后,通信两端就可以分别利用各自的net.Conn类型值交换数据了。接下来,我们来探讨golang API在net.Conn类型之上提供的功能。
这可能和我们之前说的不一样,别着急,原因如下:
Go的socket编程API我们屏蔽了相关系统的EAGAIN(eagain)的错误,使得有些socket编程API调用起来像非阻塞的。但是我们要明确,
底层使用的是非阻塞式的socket接口
好了,现在我们看net.Conn类型,它是一个接口类型,定义了可以在一个连接上做的所有事情:
# 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
}
接下来我们一起探讨下它们的方法吧。
第1个方法:
/*
* * 作用:用于从socket的接收缓冲区中读取数据
* * 参数:
* * b:用于存放从连接上接收数据的容器,长度由应用程序决定。
* * 返回值:
* * n:本次操作实际读取到的数据
*/
Read(b []byte) (n int, err error)
b: 用来接收另一方传递给conn的数据
n:就是本次操作中Read方法向参数值中填充的字节个数
err: 如果从socket的接收缓冲区读取数据的时候发现另一端的TCP已经关闭,将会返回一个error
使用示例:循环读取数据【不进行数据切割等操作】并追加到bytes.Buffer,直到另一端关闭,才把数据打印出来
func process(conn net.Conn) {
defer conn.Close()
var dataBuffer bytes.Buffer
buf := make([]byte, 1024)
for {
n, err := conn.Read(buf) // 从conn中读取客户端发送的数据内容
if(err != nil){
if err == io.EOF{
fmt.Printf("客户端退出 err=%v\n", err)
}else{
fmt.Printf("read err=%v\n", err)
}
break
}
dataBuffer.Write(buf[:n])
}
fmt.Printf(dataBuffer.String())
}
第2个方法:
/*
* * 作用:用于从socket的接收缓冲区中写数据
* * 参数:
* * b:用于存放从连接上接收数据的容器,长度由应用程序决定。
* * 返回值:
* * n:本次操作实际读取到的数据
*/
Write(b []byte) (n int, err error)
第3个方法:
/*
* 作用:关闭当前的连接
*/
Close() error
第4、5个方法:
/*
* 作用:返回本机地址, 如果没有指定,那么操作系统内核会自动分配一个
*/
LocalAddr() Addr
/*
* 作用:返回远程地址, 如果没有指定,那么操作系统内核会自动分配一个
*/
RemoteAddr() Addr
type Addr interface {
Network() string // name of the network (for example, "tcp", "udp")
String() string // string form of address (for example, "192.0.2.1:25", "[2001:db8::1]:80")
}
第6个方法:
/*
* * 作用:设定在当前连接上的读操作的超时时间
* * 参数:
* t time.Time:
* * 返回值:
* error: 如果超时时间到了操作还没有完成,就是error,提示i/o timeout。
*/
SetReadDeadline(t time.Time) error
第7个方法
/*
* * 作用:设定在当前连接上的写操作的超时时间
* * 参数:
* t time.Time:
* * 返回值:
* error: 如果超时时间到了操作还没有完成,就是error,提示i/o timeout。
*/
SetWriteDeadline(t time.Time) error
第8个方法:
/*
* * 作用:设定在当前连接上的IO操作的超时时间
* * 参数:
* t time.Time:
* * 返回值:
* error: 如果超时时间到了操作还没有完成,就是error,提示i/o timeout。
*/
SetDeadline(t time.Time) error
SetReadDeadline
+ SetWriteDeadline
conn.SetDeadline(time.Time{})
// 在 主go程中, 获取服务器回发数据。
buf2 := make([]byte, 4096)
for {
conn.SetDeadline(time.Now().Add(2*time.Second))
n, err := conn.Read(buf2)
*****
}
对于服务端,Server可能会和很多Client进行通信交互,所以我们必须保证整个Server运行状态的稳定性,因此在和Client建立连接通信的时候,确保连接的及时断开非常重要,否则一旦和多个客户端建立不关闭的长连接,对于服务器端资源的占用是非常可怕的
针对短连接,我们可以使用golang中的net包自带的timeout函数:
func (*IPConn) SetDeadline
func (c *IPConn) SetDeadline(t time.Time) error
func (*IPConn) SetReadDeadline
func (c *IPConn) SetReadDeadline(t time.Time) error
func (*IPConn) SetWriteDeadline
func (c *IPConn) SetWriteDeadline(t time.Time) error
如果我们想要给服务端设置短连接的timeout,如下:
conn, err := listen.Accept() // 创建用户数据通信的socket
if err != nil {
fmt.Println("Accept() err=", err)
conntine
} else {
fmt.Printf("主线程 %v 创建一个conn, Accept() suc con=%v 客户端 ip=%v\n", goID(), conn, conn.RemoteAddr().String())
}
conn.SetDeadline(time.Now().Add(time.Duration(10) * time.Second))
通过这样设定,每个和Server通讯的Client连接时长最长也不会超过10s了
作为长连接,由于我们往往很难确定什么时候会中断连接,因此并不能像处理短连接那样简单粗暴的设定一个timeout就可以搞定,而在Golang的net包中,并没有针对长连接的函数,因此需要我们自己设计并实现针对长连接的处理策略啦~
针对socke长连接,常见的做法是在Server和Socket之间设计通讯机制,当两者之间没有信息交互时,双方便会定时发送数据包(心跳),以维持连接状态。
这种方法是目前使用相对比较多的做法,但是开销相对也较大,特别是当Server和多个client保持长连接的时候,并发会比较高,因此还有一种策略,逻辑相对简单,开销相对较小:
当server每次收到clinet发来的消息之后,就开始心跳计时,如果在心跳计时结束之前没有再次收到Client发来的信息,那么就会断开跟Client的连接。而一旦在设计时间再次收到Client发来的信息,那么Server便会重置计时器,再次重新进行心跳计时,直到超时断开连接为止
客户端:
package main
import (
"fmt"
"net"
)
func main(){
tcpaddr,err:=net.ResolveTCPAddr("tcp4","127.0.0.1:8888")
if err!=nil{
panic(err)
}
conn,err:=net.DialTCP("tcp",nil,tcpaddr)//链接
if err!=nil{
panic(err)
}
for{
fmt.Println("阻塞等待写入数据:")
var inpustr string
fmt.Scanln(&inpustr)
conn.Write([]byte(inpustr))
/* fmt.Println("阻塞读取数据:")
buf:=make([]byte,1024)
n,_:=conn.Read(buf)//读取数据
fmt.Println(string(buf[:n]))*/
}
}
服务端:
package main
import (
"fmt"
"log"
"net"
"time"
)
//判断30秒内有没有产生通信
//超过30秒退出
func HeartBeat(conn net.Conn,heartchan chan byte,timeout int){
fmt.Println(" HeartBeat start。。。。。")
select { // 发现只要有一个case执行了,该select就会退出【不确定是不是理解的对了】
case hc:=<-heartchan:
log.Println("read chan******》",string(hc))
conn.SetDeadline(time.Now().Add(time.Duration(timeout)*time.Second)) // conn会自己接收
fmt.Println("conn deadline set finish")
/* case <-time.After(time.Duration(timeout)*time.Second) : // 这一段是没有用的
fmt.Println("kkkkk")
log.Println(conn.RemoteAddr(), "time out. will close conn")//客户端超时
conn.Close()
fmt.Println("ddddddddd")*/
}
fmt.Println(" HeartBeat end。。。。。")
}
//处理心跳的channel
func HeartChanHandler(n[]byte,beatch chan byte){
fmt.Println(" HeartChanHandler",len(n))
for _,v:=range n{
fmt.Println("put *******> chan", string(v))
beatch<-v
}
close(beatch)//关闭管道
//fmt.Println(" HeartChanHandler end, close chan")
}
func MsgHandler(conn net.Conn){
//flag := true
buf :=make([]byte,1024)
defer conn.Close()
for {
fmt.Println("阻塞等待数据读取:")
n,err:=conn.Read(buf)
if err!=nil{
fmt.Println("when read conn, conn closed")
//flag = false
break
}
msg:=buf[:n]
/*
* do something
*/
//conn.Write([]byte("收到数据:"+string(buf[1:n])+"\n"))
fmt.Println("收到数据: ", string(buf))
beatch:=make(chan byte) // beatch没有缓冲区
go HeartBeat(conn,beatch,30)
go HeartChanHandler(msg[:1],beatch) // beatch中的数据必须要及时读取完,否则会一直阻塞。导致该协程不能推出, 因此只msg的第一个数组作为心跳
}
fmt.Println("当前处理数据的协程结束。。。。")
}
func main(){
listener,err:=net.Listen("tcp","127.0.0.1:8888")
if err!=nil{
panic(err)//处理错误
}
defer listener.Close()//延迟关闭
for{
new_conn,err:=listener.Accept()//接收消息
if err!=nil{
panic(err)//处理错误
}
go MsgHandler(new_conn)//处理客户端消息
}
}
参考
服务端的处理流程
客户端的处理流程
服务端:
package main
import (
"bytes"
"fmt"
"net"
"runtime"
"strconv"
)
func main() {
listen, err := net.Listen("tcp", ":8888") // 创建用于监听的 socket
if err != nil {
fmt.Println("listen err=", err)
return
}
fmt.Println("监听套接字,创建成功, 服务器开始监听。。。")
defer listen.Close() // 服务器结束前关闭 listener
// 循环等待客户端来链接
for {
fmt.Println("阻塞等待客户端来链接...")
conn, err := listen.Accept() // 创建用户数据通信的socket
if err != nil {
fmt.Println("Accept() err=", err)
} else {
fmt.Println("通信套接字,创建成功。。。")
fmt.Printf("主线程 %v 创建一个conn, Accept() suc con=%v 客户端 ip=%v\n", goID(), conn, conn.RemoteAddr().String())
}
// 这里准备起一个协程,为客户端服务
go process(conn)
}
}
func process(conn net.Conn) {
defer conn.Close()
for {
// 创建一个新切片, 用作保存数据的缓冲区
buf := make([]byte, 1024)
fmt.Printf("服务器在等待客户端%s 发送信息, 当前线程 %v\n", conn.RemoteAddr().String(), goID())
n, err := conn.Read(buf) // 从conn中读取客户端发送的数据内容
if err != nil {
fmt.Printf("客户端退出 err=%v\n", err)
return
}
fmt.Printf("当前线程 %v, 接受消息 %s\n", goID(), string(buf[:n]))
// 回写数据给客户端
/*_, err = conn.Write([]byte("This is Server\n"))
if err != nil {
fmt.Println("Write err:", err)
return
}
*/
}
}
func goID() uint64 {
b := make([]byte, 64)
b = b[:runtime.Stack(b, false)]
b = bytes.TrimPrefix(b, []byte("goroutine "))
b = b[:bytes.IndexByte(b, ' ')]
n, _ := strconv.ParseUint(string(b), 10, 64)
return n
}
客户端:
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
func main() {
fmt.Println("建立与服务端的链接")
conn, err := net.Dial("tcp", "127.0.0.1:8888") //创建用于通信socke
if err != nil {
fmt.Println("client dial err=", err)
return
}
defer conn.Close()
reader := bufio.NewReader(os.Stdin)
for {
fmt.Println("阻塞等待终端输入数据")
line, err := reader.ReadString('\n')
if err != nil {
fmt.Println("readString err=", err)
}
fmt.Println("分析终端输入的数据")
line = strings.Trim(line, " \r\n")
if line == "exit" {
fmt.Println("客户端退出")
return
}
fmt.Println("将line发送给服务器")
_, err = conn.Write([]byte(line + "\n"))
if err != nil {
fmt.Println("conn.Write err=", err)
}
}
}
ps:也不一定要写一个客户端才能连接,可以直接通过telnet命令测试:
服务端
package main
import (
"bytes"
"fmt"
"net"
"runtime"
"strconv"
)
func main() {
listen, err := net.Listen("tcp", ":8888") // 创建用于监听的 socket
if err != nil {
fmt.Println("listen err=", err)
return
}
fmt.Println("监听套接字,创建成功, 服务器开始监听。。。")
defer listen.Close() // 服务器结束前关闭 listener
// 循环等待客户端来链接
for {
fmt.Println("阻塞等待客户端来链接...")
conn, err := listen.Accept() // 创建用户数据通信的socket
if err != nil {
fmt.Println("Accept() err=", err)
} else {
fmt.Println("通信套接字,创建成功。。。")
fmt.Printf("主线程 %v 创建一个conn, Accept() suc con=%v 客户端 ip=%v\n", goID(), conn, conn.RemoteAddr().String())
}
// 这里准备起一个协程,为客户端服务
go process(conn)
}
}
func process(conn net.Conn) {
defer conn.Close()
for {
// 创建一个新切片, 用作保存数据的缓冲区
buf := make([]byte, 1024)
fmt.Printf("服务器在等待客户端%s 发送信息, 当前线程 %v\n", conn.RemoteAddr().String(), goID())
n, err := conn.Read(buf) // 从conn中读取客户端发送的数据内容
if err != nil {
fmt.Printf("客户端退出 err=%v\n", err)
return
}
fmt.Printf("当前线程 %v, 接受消息 %s\n", goID(), string(buf[:n]))
// 回写数据给客户端
_, err = conn.Write([]byte("This is Server\n"))
if err != nil {
fmt.Println("Write err:", err)
return
}
}
}
func goID() uint64 {
b := make([]byte, 64)
b = b[:runtime.Stack(b, false)]
b = bytes.TrimPrefix(b, []byte("goroutine "))
b = b[:bytes.IndexByte(b, ' ')]
n, _ := strconv.ParseUint(string(b), 10, 64)
return n
}
package main
import (
"fmt"
"net"
"os"
)
func main() {
fmt.Println("建立与服务端的链接")
conn, err := net.Dial("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println("client dial err=", err)
return
}
// 读取用户的键盘输入。
go func() {
buf := make([]byte, 10)
for {
// 获取键盘输入。 fmt.Scan --》 结束标记 \n 和 空格
n, err := os.Stdin.Read(buf) // buf[:n]
if err != nil {
fmt.Println("os.Stdin.Read err:", err)
return
}
// 直接将读到键盘输入数据,写到 socket 中,发送给服务器
conn.Write(buf[:n])
}
}()
// 在 主go程中, 获取服务器回发数据。
buf2 := make([]byte, 4096)
for {
// 借助 socket 从服务器读取 数据。
n, err := conn.Read(buf2)
if n == 0 {
fmt.Println("客户端检查到服务器,关闭连接, 本端也退出")
return
}
if err != nil {
fmt.Println("os.Stdin.Read err:", err)
return
}
fmt.Println("客户端读到:", string(buf2[:n]))
}
}
type conn struct {
fd *netFD
}
// netFD结构体:是网络文件描述符
type netFD struct {
pfd poll.FD
// immutable until Close
family int
sotype int
isConnected bool // handshake completed or use of association with peer
net string
laddr Addr
raddr Addr
}
func (c *conn) Read(b []byte) (int, error) {
if !c.ok() {
return 0, syscall.EINVAL
}
n, err := c.fd.Read(b)
if err != nil && err != io.EOF {
err = &OpError{Op: "read", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
}
return n, err
}
func (fd *netFD) Read(p []byte) (n int, err error) {
if err := fd.readLock(); err != nil {
return 0, err
}
defer fd.readUnlock()
if err := fd.pd.PrepareRead(); err != nil {
return 0, &OpError{"read", fd.net, fd.raddr, err}
}
for {
n, err = syscall.Read(int(fd.sysfd), p)
if err != nil {
n = 0
if err == syscall.EAGAIN {
if err = fd.pd.WaitRead(); err == nil {
continue
}
}
}
err = chkReadErr(n, err, fd)
break
}
if err != nil && err != io.EOF {
err = &OpError{"read", fd.net, fd.raddr, err}
}
return
}
func (c *conn) Write(b []byte) (int, error) {
if !c.ok() {
return 0, syscall.EINVAL
}
n, err := c.fd.Write(b)
if err != nil {
err = &OpError{Op: "write", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
}
return n, err
}
func (c *conn) Close() error {
if !c.ok() {
return syscall.EINVAL
}
err := c.fd.Close()
if err != nil {
err = &OpError{Op: "close", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
}
return err
}
func (c *conn) ok() bool { return c != nil && c.fd != nil }
laiyuan