在posix标准推出后,socket在各大主流OS平台上都获得了很好的支持。而Golang是自带runtime的跨平台编程语言,Go中提供给开发者的socket API是创建在操做系统原生socket接口之上的。但golang 中的socket接口在行为特色与操做系统原生接口有一些不一样。本文将对结合一个简单的hello/hi的网络聊天程序加以分析。golang
1、socket简介web
首先进程之间能够进行通讯的前提是进程能够被惟一标识,在本地通讯时可使用PID惟一标识,而在网络中这种方法不可行,咱们能够经过IP地址+协议+端口号来惟一标识一个进程,而后利用socket进行通讯。编程
socket是位于应用层和传输层中的抽象层,它是不属于七层架构中的:服务器
而socket通讯流程以下:网络
1.服务端建立socket数据结构
2.服务端绑定socket和端口号架构
3.服务端监听该端口号并发
4.服务端启动accept()用来接收来自客户端的链接请求,此时若是有链接则继续执行,不然将阻塞在这里。dom
5.客户端建立socketsocket
6.客户端经过IP地址和端口号链接服务端,即tcp中的三次握手
7.若是链接成功,客户端能够向服务端发送数据
8.服务端读取客户端发来的数据
9.任何一端都可主动断开链接
2、socket编程
有了抽象的socket后,当使用TCP或UDP协议进行web编程时,能够经过如下的方式进行
服务端伪代码:
listenfd = socket(……)
bind(listenfd, ServerIp:Port, ……)
listen(listenfd, ……)
while(true) { conn = accept(listenfd, ……) receive(conn, ……) send(conn, ……) }
客户端伪代码:
clientfd = socket(……)
connect(clientfd, serverIp:Port, ……)
send(clientfd, data)
receive(clientfd, ……)
close(clientfd)
上述伪代码中,listenfd就是为了实现服务端监听建立的socket描述符,而bind方法就是服务端进程占用端口,避免其它端口被其它进程使用,listen方法开始对端口进行监听。下面的while循环用来处理客户端源源不断的请求,accept方法返回一个conn,用来区分各个客户端的链接的,以后的接受和发送动做都是基于这个conn来实现的。其实accept就是和客户端的connect一块儿完成了TCP的三次握手。
golang中提供了一些网络编程的API,包括Dial,Listen,Accept,Read,Write,Close等.
3.1 Listen()
首先使用服务端net.Listen()方法建立套接字,绑定端口和监听端口。
1 func Listen(network, address string) (Listener, error) { 2 var lc ListenConfig 3 return lc.Listen(context.Background(), network, address) 4 }
以上是golang提供的Listen函数源码,其中network表示网络协议,如tcp,tcp4,tcp6,udp,udp4,udp6等。address为绑定的地址,返回的Listener其实是一个套接字描述符,error中保存错误信息。
而在Linuxsocket中使用socket,bind和listen函数来完成一样功能
// socket(协议域,套接字类型,协议) int socket(int domain, int type, int protocol); int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); int listen(int sockfd, int backlog);
3.2 Dial()
当客户端想要发起某个链接时,就会使用net.Dial()方法来发起链接
func Dial(network, address string) (Conn, error) { var d Dialer return d.Dial(network, address) }
其中network表示网络协议,address为要创建链接的地址,返回的Conn实际是标识每个客户端的,在golang中定义了一个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 }
其中netFD是golang网络库里最核心的数据结构,贯穿了golang网络库全部的API,对底层的socket进行封装,屏蔽了不一样操做系统的网络实现,这样经过返回的Conn,咱们就可使用golang提供的socket底层函数了。
在Linuxsocket中使用connect函数来建立链接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
3.3 Accept()
当服务端调用net.Listen()后会开始监听指定地址,而客户端调用net.Dial()后发起链接请求,而后服务端调用net.Accept()接收请求,这里端与端的链接就创建好了,实际上到这一步也就完成了TCP中的三次握手。
Accept() (Conn, error)
golang的socket其实是非阻塞的,但golang自己对socket作了必定处理,使其看起来是阻塞的。
在Linuxsocket中使用accept函数来实现一样功能
//sockfd是服务器套接字描述符,sockaddr返回客户端协议地址,socklen_t是协议地址长度。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
3.4 Write()
端与端的链接已经创建了,接下来开始进行读写操做,conn.Write()向socket写数据
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 }
其中写入的数据是一个二进制字节流,n返回的数据的长度,err保存错误信息
Linuxsocket中对应的则是send函数
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
3.5 Read()
客户端发送完数据之后,服务端能够接收数据,golang中调用conn.Read()读取数据,源码以下:
Read(b []byte) (n int, err error)
其参数与Write()中的含义同样,在Linuxsocket中使用recv函数完成此功能
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
3.6 Close()
当服务端或者客户端想要关闭套接字时,调用Close()方法关闭链接。
Close() error
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 }
在Linuxsocket中使用close函数
int close(int socketfd)
4.1 server.go
package main import ( "fmt" "net" "strings" ) //UserMap保存的是当前聊天室全部用户id的集合 var UserMap map[string]net.Conn = make(map[string]net.Conn) func main() { //监听本地全部ip的8000端口 listen_socket, err := net.Listen("tcp", "127.0.0.1:8000") if err != nil { fmt.Println("服务启动失败") } //关闭监听的端口 defer listen_socket.Close() fmt.Println("等待用户加入聊天室") for { //用于conn接收连接 conn, err := listen_socket.Accept() if err != nil { fmt.Println("链接失败") } //打印加入聊天室的公网IP地址 fmt.Println(conn.RemoteAddr(), "链接成功") //定义一个goroutine,这里主要是为了并发运行 go DataProcessing(conn) } } func DataProcessing(conn net.Conn) { for { //定义一个长度为255的切片 data := make([]byte, 255) //读取客户端传来的数据,msg_length保存长度,err保存错误信息 msg_length, err := conn.Read(data) if msg_length == 0 || err != nil { continue } //解析协议,经过分隔符"|"获取须要的数据,msg_str[0]存放操做类别 //msg_str[1]存放用户名,msg_str[2]若是有就存放发送的消息 msg_str := strings.Split(string(data[0:msg_length]), "|") switch msg_str[0] { case "nick": fmt.Println(conn.RemoteAddr(), "的用户名是", msg_str[1]) for user, message := range UserMap { //向除本身以外的用户发送加入聊天室的消息 if user != msg_str[1] { message.Write([]byte("用户" + msg_str[1] + "加入聊天室")) } } //将该用户加入用户id的集合 UserMap[msg_str[1]] = conn case "send": for user, message := range UserMap { if user != msg_str[1] { fmt.Println("Send "+msg_str[2]+" to ", user) //向除本身以外的用户发送聊天消息 message.Write([]byte(" 用户" + msg_str[1] + ": " + msg_str[2])) } } case "quit": for user, message := range UserMap { if user != msg_str[1] { //向除本身以外的用户发送退出聊天室的消失 message.Write([]byte("用户" + msg_str[1] + "退出聊天室")) } } fmt.Println("用户 " + msg_str[1] + "退出聊天室") //将该用户名从用户id的集合中删除 delete(UserMap, msg_str[1]) } } }
5.2 client.go
package main import ( "bufio" "fmt" "net" "os" ) var nick string = "" func main() { //拨号操做 conn, err := net.Dial("tcp", "127.0.0.1:8000") if err != nil { fmt.Println("链接失败") } defer conn.Close() fmt.Println("链接服务成功 \n") //建立用户名 fmt.Printf("在进入聊天室以前给本身取个名字吧:") fmt.Scanf("%s", &nick) fmt.Println("用户" + nick + "欢迎进入聊天室") //向服务器发送数据 conn.Write([]byte("nick|" + nick)) //定义一个goroutine,这里主要是为了并发运行 go SendMessage(conn) var msg string for { msg = "" //因为golangz的fmt包输入字符串不能读取空格,因此此处重写了一个Scanf函数 Scanf(&msg) if msg == "quit" { //这里的quit,send,以及上面的nick是为了识别客户端作的是设置用户名,发消息仍是退出 conn.Write([]byte("quit|" + nick)) break } if msg != "" { conn.Write([]byte("send|" + nick + "|" + msg)) } } } func SendMessage(conn net.Conn) { for { //定义一个长度为255的切片 data := make([]byte, 255) //读取服务器传来的数据,msg_length保存长度,err保存错误信息 msg_length, err := conn.Read(data) if msg_length == 0 || err != nil { break } fmt.Println(string(data[0:msg_length])) } } //重写的Scanf函数 func Scanf(a *string) { reader := bufio.NewReader(os.Stdin) data, _, _ := reader.ReadLine() *a = string(data) }
golang中使用goroutine实现并发
5.3 运行截图
多人聊天截图(左上角为服务端)
用户退出聊天室(左上角为服务端)
参考资料:
https://tonybai.com/2015/11/17/tcp-programming-in-golang/