卷毛-网络编程基础(二)什么是socket

Socket编程

什么是Socket

Socket,英文含义是【插座、插孔】,一般称之为套接字,用于描述IP地址和端口。可以实现不同程序间的数据通信。

Socket起源于Unix,而Unix基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用:Socket(),该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。

套接字的内核实现较为复杂,我也不是很了解,了解到如下结构足矣。

套接字通讯原理示意
在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。

常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。

网络应用程序设计模式

C/S模式

传统的网络应用设计模式,客户机(client)/服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。例如LOL

B/S模式

浏览器(Browser)/服务器(Server)模式。只需在一端部署服务器,而另外一端使用每台PC都默认配置的浏览器即可完成数据的传输。例如:页游

Golang TCP的C/S架构

在这里插入图片描述

简单的C/S模型通信

Server端:

func Listen(network, address string) (Listener, error)
//network:选用的协议:TCP、UDP, 	如:“tcp”或 “udp”
//address:IP地址+端口号, 			如:“127.0.0.1:8000”或 “:8000”

//Listener 接口:
type Listener interface {
	Accept() (Conn, error)
	Close() error
	Addr() Addr
}
//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
}

参看 https://studygolang.com/pkgdoc 中文帮助文档中的demo:

示例代码:
TCP服务器.go

package main

import (
   "net"
   "fmt"
)

func main()  {
   // 创建监听
   listener, err:= net.Listen("tcp", ":8000")
   if err != nil {
      fmt.Println("listen err:", err)
      return
   }
   defer listener.Close()          // 主协程结束时,关闭listener

   fmt.Println("服务器等待客户端建立连接...")
   // 等待客户端连接请求
   conn, err := listener.Accept()
   if err != nil {
      fmt.Println("accept err:", err)
      return
   }
   defer conn.Close()             // 使用结束,断开与客户端链接
   fmt.Println("客户端与服务器连接建立成功...")

   // 接收客户端数据
   buf := make([]byte, 1024)        // 创建1024大小的缓冲区,用于read
   n, err := conn.Read(buf)
   if err != nil {
      fmt.Println("read err:", err)
      return
   }
   fmt.Println("服务器读到:", string(buf[:n])) // 读多少,打印多少。
}

如图,在整个通信过程中,服务器端有两个socket参与进来,但用于通信的只有 conn 这个socket。它是由 listener创建的。隶属于服务器端。
在这里插入图片描述
Client 端:

//Dial函数:
func Dial(network, address string) (Conn, error)
//network:选用的协议:TCP、UDP,如:“tcp”或 “udp”
//address:服务器IP地址+端口号, 如:“121.36.108.11:8000”或 “www.itcast.cn:8000”
//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
}

client.go

package main

import (
   "net"
   "fmt"
)

func main() {
   // 主动发起连接请求
   conn, err := net.Dial("tcp", "127.0.0.1:8000")
   if err != nil {
      fmt.Println("Dial err:", err)
      return
   }
   defer conn.Close()         // 结束时,关闭连接

   // 发送数据
   _, err = conn.Write([]byte("Are u ready?"))
   if err != nil {
      fmt.Println("Write err:", err)
      return
   }
}
并发的C/S模型通信
并发Server

现在已经完成了客户端与服务端的通信,但是服务端只能接收一个用户发送过来的数据,怎样接收多个客户端发送过来的数据,实现一个高效的并发服务器呢?

Accept()函数的作用是等待客户端的链接,如果客户端没有链接,该方法会阻塞。如果有客户端链接,那么该方法返回一个Socket负责与客户端进行通信。所以,每来一个客户端,该方法就应该返回一个Socket与其通信,因此,可以使用一个死循环,将Accept()调用过程包裹起来。

需要注意的是,实现并发处理多个客户端数据的服务器,就需要针对每一个客户端连接,单独产生一个Socket,并创建一个单独的goroutine与之完成通信。

package main

import (
	"net"
	"fmt"
	"strings"
)

func HandlerConnect(conn net.Conn) {

	defer conn.Close()

	// 获取连接的客户端 Addr
	addr := conn.RemoteAddr()
	fmt.Println(addr, "客户端成功连接!")

	// 循环读取客户端发送数据
	buf := make([]byte, 4096)
	for {
		n, err := conn.Read(buf)
		//fmt.Println(buf[:n])
		if "exit\n" == string(buf[:n]) || "exit\r\n" == string(buf[:n]) {
			fmt.Println("服务器接收的客户端退出请求,服务器关闭")
			return
		}
		if n == 0 {
			fmt.Println("服务器检测到客户端已关闭,断开连接!!!")
			return
		}
		if err != nil {
			fmt.Println("conn.Read err:", err)
			return
		}
		fmt.Println("服务器读到数据:", string(buf[:n]))	// 使用数据

		// 小写转大写,回发给客户端
		conn.Write([]byte(strings.ToUpper(string(buf[:n]))))
	}
}

func main()  {
	// 创建监听套接字
	//listener, err := net.Listen("tcp", "127.0.0.1:8001")
	listener, err := net.Listen("tcp", "192.168.15.78:8001")
	if err != nil {
		fmt.Println("net.Listen err:", err)
		return
	}
	defer listener.Close()

	// 监听客户端连接请求
	for {
		fmt.Println("服务器等待客户端连接...")
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("listener.Accept err:", err)
			return
		}
		// 具体完成服务器和客户端的数据通信
		go HandlerConnect(conn)
	}
}

将客户端的数据处理工作封装到HandleConn方法中,需将Accept()返回的Socket传递给该方法,变量conn的类型为:net.Conn。可以使用conn.RemoteAddr()来获取成功与服务器建立连接的客户端IP地址和端口号:

并发Client

客户端不仅需要持续的向服务端发送数据,同时也要接收从服务端返回的数据。因此可将发送和接收放到不同的协程中。

主协程循环接收服务器回发的数据(该数据应已转换为大写),并打印至屏幕;子协程循环从键盘读取用户输入数据,写给服务器。读取键盘输入可使用 os.Stdin.Read(str)。定义切片str,将读到的数据保存至str中。

这样,客户端也实现了多任务。

package main

import (
	"net"
	"fmt"
	"os"
)

func main()  {
	// 主动发起连接请求
	conn, err := net.Dial("tcp", "192.168.15.78:8001")
	if err != nil {
		fmt.Println("net.Dial err:", err)
		return
	}
	defer conn.Close()
	// 获取用户键盘输入( stdin ),将输入数据发送给服务器
	go func() {
		str := make([]byte, 4096)
		for {
			n, err := os.Stdin.Read(str)
			if err != nil {
				fmt.Println("os.Stdin.Read err:", err)
				continue
			}
			//写给服务器, 读多少,写多少!
			conn.Write(str[:n])
		}
	}()

	// 回显服务器回发的大写数据
	buf := make([]byte, 4096)
	for {
		n, err := conn.Read(buf)
		if n == 0 {
			fmt.Println("检查到服务器关闭,客户端也关闭")
			return
		}
		if err != nil {
			fmt.Println("conn.Read err:", err)
			return
		}
		fmt.Println("客户端读到服务器回发:", string(buf[:n]))
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值