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

存入通道的顺序必须与从通道中取数据的顺序是相同的