并发C/S模型通信-最后有练习题
并发C/S模型通信
并发Server
现在已经完成了客户端与服务端的通信,但是服务端只能接收一个用户发送过来的数据,怎样接收多个客户端发送过来的数据,实现一个高效的并发服务器呢?
Accept()函数的作用是等待客户端的链接,如果客户端没有链接,该方法会阻塞。如果有客户端链接,那么该方法返回一个Socket负责与客户端进行通信。所以,每来一个客户端,该方法就应该返回一个Socket与其通信,因此,可以使用一个死循环,将Accept()调用过程包裹起来。
需要注意的是,实现并发处理多个客户端数据的服务器,就需要针对每一个客户端连接,单独产生一个Socket,并创建一个单独的goroutine与之完成通信。
//监听
listener, err := net.Listen("tcp", "127.0.0.1:8001") // tcp 不能使用大写
if err != nil {
fmt.Println("err = ", err)
return
}
defer listener.Close()
//接收多个用户
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("err = ", err)
return
}
//处理用户请求, 新建一个go程
go HandleConn(conn)
}
将客户端的数据处理工作封装到HandleConn方法中,需将Accept()返回的Socket传递给该方法,变量conn的类型为:net.Conn。可以使用conn.RemoteAddr()来获取成功与服务器建立连接的客户端IP地址和端口号:
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
}
//获取客户端的网络地址信息
addr := conn.RemoteAddr().String()
fmt.Println(addr, " conncet sucessful")
客户端可能持续不断的发送数据,因此接收数据的过程可以放在for循环中,服务端也持续不断的向客户端返回处理后的数据。
添加一个限定,如果客户端发送一个“exit”字符串,表示客户端通知服务器不再向服务端发送数据,此时应该结束HandleConn方法,同时关闭与该客户端关联的Socket。
buf := make([]byte, 2048) //创建一个切片,存储客户端发送的数据
**for** {
//读取用户数据
n, err := conn.Read(buf)
**if** err != nil {
fmt.Println("err = ", err)
**return**
}
fmt.Printf("[%s]: %s\n", addr, string(buf[:n]))
**if** "exit" == string(buf[:n-2]) { //自己写的客户端测试, 发送时,多了2个字符, "\r\n"
fmt.Println(addr, " exit")
**return**
}
//服务器处理数据:把客户端数据转大写,再写回给client
conn.Write([]byte(strings.ToUpper(string(buf[:n]))))
}
在上面的代码中,Read()方法获取客户端发送过来的数据,填充到切片buf中,返回的是实际填充的数据的长度,所以将客户端发送过来的数据进行打印,打印的是实际接收到的数据。
fmt.Printf("[%s]: %s\n", addr, string(buf[:n])).同时也可以将客户端的网络地址信息打印出来。
在判断客户端数据是否为“exit”字符串时,要注意,客户端会自动的多发送2个字符:“\r\n”(这在windows系统下代表回车、换行) linux系统下是n-1
Server使用Write方法将数据写回给客户端,参数类型是 []byte,需使用strings包下的ToUpper函数来完成大小写转换。转换的对象即为string(buf[:n])
综上,HandleConn方法完整定义如下:
//处理用户请求
func HandleConn(conn net.Conn) {
//函数调用完毕,自动关闭conn
defer conn.Close()
//获取客户端的网络地址信息
addr := conn.RemoteAddr().String()
fmt.Println(addr, " conncet sucessful")
buf := make([]byte, 2048)
for {
//读取用户数据
n, err := conn.Read(buf)
if err != nil {
fmt.Println("err = ", err)
return
}
fmt.Printf("[%s]: %s\n", addr, string(buf[:n]))
fmt.Println("len = ", len(string(buf[:n])))
//if "exit" == string(buf[:n-1]) { // nc测试,发送时,只有 \n
if "exit" == string(buf[:n-2]) { // 自己写的客户端测试, 发送时,多了2个字符, "\r\n"
fmt.Println(addr, " exit")
return
}
//把数据转换为大写,再给用户发送
conn.Write([]byte(strings.ToUpper(string(buf[:n]))))
}
}
练习题
实现类似nc 命令的客户端程序(IP地址和端口号可设为固定),能动态获取用户键盘输入数据,
并将用户输入的实际数据发送给服务器。获取到服务器回发的数据(如大写)后,显示到屏幕。
当用户键入 “exit”时,退出当前程序,并关闭与服务器的连接。