client.go
package main
import (
"bufio"
"fmt"
"net"
"os"
)
//让用户能后续输入数据
func scandata(conn net.Conn) {
//for{}死循环保证用户不止能输入一次数据
for {
//使用bufio包结合缓冲区获取输入,可以使输入不受空格的影响
inputReader := bufio.NewReader(os.Stdin)
buf, _, _ := inputReader.ReadLine()
//当用户输入q,则停止输入
if string(buf) == "q" {
os.Exit(0)
}
//将客户端用户输入的数据写入conn,让服务端去读取
conn.Write(buf)
}
}
func main() {
//创建客户端,指定接口类型为tcp,IP地址为127.0.0.1,端口号为9909
conn, _ := net.Dial("tcp", "127.0.0.1:9909")
//设置每次从连接conn读取的字节数
buf := make([]byte, 1024)
//获取参数数组
args := os.Args
//获取第一个参数(即用户名,如go run clinet1.go 阿萨德,这里的args[1]就是阿萨德),再转换成字节数组,conn的读写数据只接受字节数组
conn.Write([]byte(args[1]))
//开启一个协程,让用户能后续输入数据
go scandata(conn)
//for{}死循环保证不止读取一次服务端写入的数据,只要服务端写入数据,客户端就能读取
for {
//conn.Read方法是一个同步阻塞的方法,就是说如果conn中没数据可读,下面的代码就不会执行
n, _ := conn.Read(buf)
//这里吧客户端读取到的数据转换为字节数组输出
fmt.Println(string(buf[:n]))
}
}
server1.go
package main
import (
"fmt"
"net"
"strings"
)
//声明一个客户端的结构体
type client struct {
//客户端的连接
conn net.Conn
//客户端的用户名
name string
}
//存放要发送给所有客户端的消息
var ch_all chan string = make(chan string)
//存放要发给某一个客户端的消息
var ch_one chan string = make(chan string)
//存放要发送的客户端的用户名
var ch_who chan string = make(chan string)
//客户端列表,key是IP+端口,value是客户端结构体
var users map[string]client = make(map[string]client)
//处理客户端存在conn的数据
func handleConn(conn net.Conn) {
//在函数结束时关闭连接conn
defer conn.Close()
//设置每次从连接conn读取的字节数
buf := make([]byte, 100)
//读取客户端用户第一次进入聊天室写入的数据
n, _ := conn.Read(buf)
//获取客户端的用户名
name := string(buf[:n])
//创建客户端结构体存储数据
var oneclient client
oneclient.conn = conn
oneclient.name = name
key := conn.RemoteAddr().String()
//添加客户端结构体
users[key] = oneclient
msg := name + "进入聊天室"
//将第一次进入聊天室的消息放入要发送给所有客户端的通道
ch_all <- msg
//for{}死循环保证服务端可以不断得读取该客户端的数据,并与它做数据交互
for {
//服务端读取客户端手动输入的数据
n, _ = conn.Read(buf)
//如果连接断开,n==0,连接断开有两种可能:1.客户端手动把自己的程序关了2.服务端通过conn.Close()关了
if n == 0 {
fmt.Println(key + "断开连接")
msg2 := name + "离开聊天室"
//删除客户端列表中的客户端
delete(users, key)
//将客户端用户离开聊天室的消息群发给每一个客户端
ch_all <- msg2
//关闭处理该客户端的协程
return
}
//如果客户端输入的第一个字符是@,代表要单发给某一个客户端
if string(buf[:n])[0] == '@' {
//先去除@,再用空格分隔字符串
sli := strings.Fields(string(buf[1:n]))
//获取字符串数组的第一个元素,这个就是客户端的用户名
who := sli[0]
//后边的再拼接回去
msg = strings.Join(sli[1:], " ")
//把要单发的消息和客户端的用户名分别存到对应的通道
ch_who <- who
ch_one <- name + "->me : " + msg
//跳过,否则会执行下面的群发
continue
}
//把要群发的消息存到相应的通道
ch_all <- name + "->all:" + string(buf[:n])
}
}
//单发
func send_one() {
//for{}死循环保证可以不断地处理通道的数据
for {
//获取数据
who := <-ch_who
msg := <-ch_one
//遍历客户端列表,找到相应的客户端用户名,把群发消息写入到conn
for _, user := range users {
if who == user.name {
user.conn.Write([]byte(msg))
break
}
}
}
}
//群发
func send_all() {
for{}死循环保证可以不断地处理通道的数据
for {
//获取数据
msg := <-ch_all
//遍历客户端列表,把消息写入到每一个客户端的conn
for _, user := range users {
user.conn.Write([]byte(msg))
}
}
}
func main() {
//开启服务端
listen, _ := net.Listen("tcp", ":9909")
//函数结束时自动关闭
defer listen.Close()
//开启单发协程
go send_all()
//开启群发协程
go send_one()
//for{}死循环保证不断接受客户端的连接
for {
conn, _ := listen.Accept()
fmt.Println(conn.RemoteAddr().String())
//开启处理连接的客户端的协程
go handleConn(conn)
}
}
分析:
里面有一个值得注意的地方,在server.go的handleConn和send_one中,分别有这两个地方
//把要单发的消息和客户端的用户名分别存到对应的通道
ch_who <- who
ch_one <- name + "->me : " + msg
//获取数据
who := <-ch_who
msg := <-ch_one
存入通道的顺序必须与从通道中取数据的顺序是相同的