TCP socket编程 实现客户端和服务端的通信

明确两个部分各需要做什么事情

服务端 客户端

服务端的处理流程

1.监听接口

2.接收客户端的tcp连接,建立客户端和服务端的链接

3.创建goroutine,处理该链接的请求(通常客户端会通过连接发送请求包)。使用goroutine能够处理很多的请求。

客户端的处理流程

1.建立于服务端的链接

2.发送请求数据,接受服务器端返回的结果数据

3.关闭链接

服务器端和n个客户端服务的示意图

每来一个客户端都能够开一个协程来进行处理

服务器端的功能

1.编写一个服务器端程序,在8888端口监听

2.可以和多个客户端创建链接

3.链接成功后,客户端可以发送数据,服务器端接受数据并显示在终端上

4.先使用telnet来测试,然后编写客户端程序来测试

Listen函数创建的服务端: 

ln, err := net.Listen("tcp", ":8080")
if err != nil {
	// handle error
}
for {
	conn, err := ln.Accept()
	if err != nil {
		// handle error
		continue
	}
	go handleConnection(conn)
}

返回在一个本地网络地址laddr上监听的Listener。网络类型参数net必须是面向流的网络:

"tcp"、"tcp4"、"tcp6"、"unix"或"unixpacket"。参见Dial函数获取laddr的语法。

func Listen
func Listen(net, laddr string) (Listener, error)

使用案例: 

// Listen on TCP port 2000 on all interfaces.
l, err := net.Listen("tcp", ":2000")
if err != nil {
    log.Fatal(err)
}
defer l.Close()
for {
    // Wait for a connection.
    conn, err := l.Accept()
    if err != nil {
        log.Fatal(err)
    }
    // Handle the connection in a new goroutine.
    // The loop then returns to accepting, so that
    // multiple connections may be served concurrently.
    go func(c net.Conn) {
        // Echo all incoming data.
        io.Copy(c, c)
        // Shut down the connection.
        c.Close()
    }(conn)
}

type Listener
type Listener interface {
    // Addr返回该接口的网络地址
    Addr() Addr
    // Accept等待并返回下一个连接到该接口的连接
    Accept() (c Conn, err error)
    // Close关闭该接口,并使任何阻塞的Accept操作都会不再阻塞并返回错误。
    Close() error
}

开始实战的代码: 

Listen会返回一个listener和一个error错误信息,用listen和err来接受。

func main() {
	fmt.Println("服务器开始监听。。。")
	
	//1.第一个参数tcp表示网络协议是tcp
	//2.第二个参数0.0.0.0:8888 表示在本地监听8888端口
	listen, err := net.Listen("tcp", "0.0.0.0:8888")

如果,err不为空的话,就说明是有错误的,要打印出来监听所产生的错误,然后直接return。 

package main

import (
	"fmt"
	"net"
)

func main() {
	fmt.Println("服务器开始监听。。。")

	//1.第一个参数tcp表示网络协议是tcp
	//2.第二个参数0.0.0.0:8888 表示在本地监听8888端口
	Listen, err := net.Listen("tcp", "0.0.0.0:8888")
	if err != nil {
		//err不为空就表示失败了
		fmt.Println("listen err=", err)
		//一旦监听都不成功就别干了,就直接失败了,就直接return了
		return
	}
	fmt.Printf("Listen suc=%v\n", Listen)
}

 先打印出来,看看Listen是个什么东西,但是发现当调用完这个函数后,就直接停止了,就不会继续监听了,所以需要想个别的办法让server继续监听,不要让他直接就停止掉。

 这时候注意到,官方给的Listener的字段方法,第二个方法Accept会等待连接,不让这个监听直接就关上。

type Listener interface {
    // Addr返回该接口的网络地址
    Addr() Addr
    // Accept等待并返回下一个连接到该接口的连接
    Accept() (c Conn, err error)
    // Close关闭该接口,并使任何阻塞的Accept操作都会不再阻塞并返回错误。
    Close() error
}

Accept返回的是一个Conn类型的c,查看Conn详细内容,发现Conn可以读可以写,可以关闭,可以返回本地的地址,也可以返回远程网络的地址。

type Conn interface {
    // Read从连接中读取数据
    // Read方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真
    Read(b []byte) (n int, err error)
    // Write从连接中写入数据
    // Write方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真
    Write(b []byte) (n int, err error)
    // Close方法关闭该连接
    // 并会导致任何阻塞中的Read或Write方法不再阻塞并返回错误
    Close() error
    // 返回本地网络地址
    LocalAddr() Addr
    // 返回远端网络地址
    RemoteAddr() Addr
    // 设定该连接的读写deadline,等价于同时调用SetReadDeadline和SetWriteDeadline
    // deadline是一个绝对时间,超过该时间后I/O操作就会直接因超时失败返回而不会阻塞
    // deadline对之后的所有I/O操作都起效,而不仅仅是下一次的读或写操作
    // 参数t为零值表示不设置期限
    SetDeadline(t time.Time) error
    // 设定该连接的读操作deadline,参数t为零值表示不设置期限
    SetReadDeadline(t time.Time) error
    // 设定该连接的写操作deadline,参数t为零值表示不设置期限
    // 即使写入超时,返回值n也可能>0,说明成功写入了部分数据
    SetWriteDeadline(t time.Time) error
}

 新加入的代码如下,可以循环监听,在for循环中有Accept,这样可以循环的接受到新的连接,在for循环中,准备一个位置要起一个协程,这个协程是来为客服端进行服务的。

	defer Listen.Close() //延时关闭listen

	//循环的等待,等待的是客户端的连接
	for {
		fmt.Println("等待客户端来连接。。。")
		conn, err := Listen.Accept()
		if err != nil {
			//这里出现错误,是Accept出错了,哪里出现的错误要体现出来
			fmt.Println("Accept err=", err)
			return
		} else {
			fmt.Printf("Accept() con=%v,ip=%v\n", conn, conn.RemoteAddr().String())
		}
		//这里准备起一个协程,来为客户端进行服务
		//用telnet来进行测试连接

	}

因为还没有客户端,所以来使用telnet来进行测试 

在这里,for又蹦出来一个等待客户端的连接,说明这个telnet指令连接成功了。

下面开始写client,

conn, err := net.Dial("tcp", "0.0.0.0:8888")

如果成功,会建立一个连接,返回一个conn。 

package main

import (
	//"bufio"
	"fmt"
	"net"
	//"os"
	//"strings"
)

func main() {
	//Dial,你用tcp我也用tcp,
	//返回这个连接
	conn, err := net.Dial("tcp", "0.0.0.0:8888")
	if err != nil {
		fmt.Println("client dial err=", err)
		//连都连不上一定要return
		return
	}
	fmt.Println("conn 成功=", conn)
}

fmt.Printf("Accept() con=%v,ip=%v\n", conn, conn.RemoteAddr().String())

 查看Add,发现Add是一个结构类型的结构体,其中有个String()字段是string类型的,于是.String()就能调出来这个结构体的地址字段。

type Addr
type Addr interface {
    Network() string // 网络名
    String() string  // 字符串格式的地址
}

 给客户端加一个功能,可以发送单行数据,并退出。

1,首先调用bufio中的NewReader来读入键盘输入的东西,然后使用conn中的Write来写入,因为需要写入的是byte类型的,所以需要进行格式的强制转换。

	//功能一:客户端可以发送单行数据,然后就退出
	reader := bufio.NewReader(os.Stdin) //Stdin代表标准输入,即【终端】
	//从终端读取到一行用户的输入,准备发送给服务器
	line, err := reader.ReadString('\n')
	if err != nil {
		fmt.Println("readString err=", err)
	}
	//type Conn interface {
	// Read从连接中读取数据
	// Read方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真
	//Read(b []byte) (n int, err error)
	// Write从连接中写入数据
	// Write方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真
	//Write(b []byte) (n int, err error)
	n, err := conn.Write([]byte(line + "\n"))
	//从上面来看,conn.Write会返回一个n,代表的是字节的长度
	if err != nil {
		fmt.Println("conn.Writer err=", err)
	}
	fmt.Printf("客户端发送了%d字节的数据,并退出", n)

 process接受的是conn,可以对conn的数据进行处理。

func process(conn net.Conn) {
	//在这里,可以循环的接受客户端发送的数据

	//在这里,需要关闭,如果不关闭的话,连接会越来越多
	defer conn.Close() //关闭这个conn
	for {
		//每次都要创建一个新的切片
		buf := make([]byte, 1024)
		//conn.Read(buf)
		//1.等待客户端通过conn发送信息
		//2.如果客户端没有writer[发送],那么这个协程就阻塞在这里
		//fmt.Printf("服务器在等待客户端发送信息%s\n", conn.RemoteAddr().String())

		//Read(b []byte) (n int, err error),read需要一个切片
		n, err := conn.Read(buf) //从conn读取
				// Read reads data from the connection.
				// Read can be made to time out and return an error after a fixed
				// time limit; see SetDeadline and SetReadDeadline.
				//Read(b []byte) (n int, err error)
		if err != nil {
			fmt.Println("服务器端的Read err=", err)
			return
		}
		//3.显示客户端发送的内容到服务器的终端
		fmt.Print(string(buf[:n])) //n 代表的是从管道中真正读到的内容是什么
		//为什么要加一个n,因为buf是开了一个空间,如果不到n的话,可能会输出很多的空格
	}

}

客户端输入过数据之后,客户端会退出,这时候err就有了,那么服务端就会报错。 

 代码实现在这里:

		if err != nil {
			fmt.Println("服务器端的Read err=", err)
			return//在这里,client的退出也会报错
		}

 实现客户端的循环输入,进行向服务器端发送信息

将之前的语句嵌套进一个for中就实现了循环的输入

for {
		//从终端读取到一行用户的输入,准备发送给服务器
		line, err := reader.ReadString('\n')
		if err != nil {
			fmt.Println("readString err=", err)
		}
		//因为上面用户输入的是加上了\n,所以这里需要去掉
		line = strings.Trim(line, " \r\n")
		//如果用户输入的是exit 就退出
		if line == "exit" {
			fmt.Println("客户端退出了")
			break
		}

		//type Conn interface {
		// Read从连接中读取数据
		// Read方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真
		//Read(b []byte) (n int, err error)
		// Write从连接中写入数据
		// Write方法可能会在超过某个固定时间限制后超时返回错误,该错误的Timeout()方法返回真
		//Write(b []byte) (n int, err error)
		_, err = conn.Write([]byte(line + "\n"))
		//从上面来看,conn.Write会返回一个n,代表的是字节的长度
		if err != nil {
			fmt.Println("conn.Writer err=", err)
		}
	}

 效果如下图

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值