网络编程

53 阅读8分钟

一、网络概念

官方解释:网络是指一组彼此连接并能够相互发送数据的主机

1. 以访问一个网站为例

image.png

浏览器输入网址“www.baidu.com”

  1. 解析域名。www.baidu.com是域名,需要找到对应的IP
    • 先去本地Host查看是否有对应域名的IP,查询到就返回
    • 查询不到就访问DNS服务器,向上级层层发送请求直到查询到IP,然后返回对应域名地址
  2. 请求服务。将获得的IP等端口信息进行Tcp封包,使用自身网卡将封包的数据通过wifi或蜂窝数据网络等发送给网络节点,经过一系列有协议的传输到达目标服务器。之后目标服务器发送服务,浏览器根据数据渲染界面

所有的通信网络设备都有一个IP地址,并且网络设备都是通过一根线连接起来进行通信,通信必须符合规则(也就是协议)。根据通信规模的大小,粗略划分为广域网WAN 和局域网LAN。互联网的核心是”互联网协议“,为了实现统一的标准,定义了OSI模型

层次核心作用设备/协议数据单位通俗类比
7. 应用层直接给用户 / 应用程序提供网络服务(比如浏览网页、发邮件)HTTP、HTTPS、FTP、SMTP、DNS应用数据(报文)用户(寄件人 / 收件人):明确要寄的东西
6. 表示层处理数据的 “格式转换 + 加密”(比如把文字转成字节、HTTPS 加密数据)加密算法(AES/RSA)、字符编码(UTF-8)应用数据(经处理)把物品打包、加密
5. 会话层建立 / 维护 / 终止 “通信会话”(比如确认双方是否在线、断开连接时清理资源)TCP 会话、RPC(远程调用)应用数据确认寄件 / 收件人身份、协调运输时机
4. 传输层负责 “端到端的可靠传输”(比如确保数据不丢失、不重复、按顺序到达)TCP(可靠)、UDP(不可靠)段(Segment)选择运输方式(顺丰 = TCP,普通快递 = UDP)、跟踪包裹
3. 网络层负责 “跨网段路由”(找数据从 A 到 B 的最佳路径)IP、路由器、ICMP(ping 命令用的)分组(Packet)根据地址选路线(北京→上海→杭州)
2. 数据链路层把网络层的数据包转成物理信号,处理局域网内的设备识别交换机、MAC 地址、以太网协议(Ethernet)帧(Frame)投递快递(按门牌号 = MAC 地址)
1. 物理层把二进制数据转成电信号 / 光信号,通过网线、光纤等介质传递网卡、网线、光纤、集线器、Wi-Fi 信号比特(Bit)卡车+ 公路

2. TCP/IP模型

OSI模型主要用于理论,互联网常见实用的模型是TCP/IP模型,它们两个可以类比但实际不一样

image.png

3. 软硬件角度

程序员只需要在客户端/服务端完成代码的工作,并且调用套接字接口即可。TCP/IP封装在了内核代码中,之后的一系列底层操作都不用管

二、网络编程

网络编程就是基于各种协议,让数据在网络设备中传输和通信。简单说,就是写代码让两台(或多台)设备 “对话”,支撑网页访问、聊天、文件传输等所有网络功能

TCP编程

 基于TCP 协议(可靠、面向连接),通过 Socket 接口实现客户端与服务器的双向通信

服务端

  • 监听端口
  • 接受客户端请求建立连接
  • 创建协程处理连接
package main  
  
import (  
    "bufio"  // 提供带缓冲的读写工具,提升IO操作效率(尤其适合频繁小数据读写)  
    "log"    // 日志输出包,自带时间戳,比fmt更适合调试和记录程序运行状态  
    "net"    // 网络编程核心包,提供TCP/UDP监听、连接、读写等底层能力  
)  
  
// main函数:程序入口,TCP服务器的核心启动逻辑  
func main() {  
    // 1. 创建TCP监听器,绑定本地地址和端口  
    // 参数1: "tcp" 指定使用TCP协议  
    // 参数2: "127.0.0.1:20000" 监听地址,127.0.0.1是本地回环地址(仅本机可访问),20000是端口号  
    // 返回值listen: 监听器对象,用于后续接受客户端连接;err: 监听过程中的错误(如端口被占用)  
    listen, err := net.Listen("tcp", "127.0.0.1:20000")  
    if err != nil {  
       // 监听失败则打印日志并终止程序(log.Fatal等价于log.Print+os.Exit(1))  
       log.Fatal(err)  
    }  
  
    // 2. 无限循环接受客户端连接(服务器持续运行,永不退出)  
    for {  
       // 阻塞等待客户端连接:程序运行到这里会暂停,直到有客户端发起连接请求  
       // 返回值conn: 连接对象,代表服务器与当前客户端的双向通信通道;err: 连接建立过程中的错误  
       conn, err := listen.Accept()  
       if err != nil {  
          // 连接建立失败(如客户端异常断开),打印日志后继续等待下一个连接  
          // 注意:生产环境建议用log.Println而非log.Fatal,避免单个连接失败导致服务器崩溃  
          log.Println("接受客户端连接失败:", err)  
          continue // 跳过当前错误连接,继续等待下一个  
       }  
  
       // 3. 启动协程(goroutine)处理当前客户端的通信逻辑  
       // 关键点:协程轻量高效,使服务器能同时处理多个客户端连接(并发核心)  
       // 每个客户端连接对应一个独立协程,互不干扰(比如同时处理100个客户端就启动100个协程)  
       go process(conn)  
    }  
}  
  
// process函数:处理单个客户端的读写通信逻辑(每个客户端连接对应一个协程执行此函数)  
// 参数conn: 与客户端的连接对象,通过它实现数据读写  
func process(conn net.Conn) {  
    // 延迟关闭连接:函数执行完毕(无论正常结束还是异常退出)时自动关闭conn  
    // 必须添加!避免客户端断开后服务器连接资源泄漏(端口、内存占用)  
    defer conn.Close()  
  
    // 无限循环读取客户端发送的数据(持续与客户端通信,直到连接断开)  
    for {  
       // 1. 基于连接创建带缓冲的读取器:减少系统调用次数,提升小数据读取效率  
       reader := bufio.NewReader(conn)  
  
       // 2. 定义数据缓冲区:用于存储从客户端读取的字节数据  
       // [1024]byte:创建固定大小为1024字节的数组,最多一次读取1024字节(超出部分分多次读取)  
       var buf [1024]byte  
  
       // 3. 从客户端读取数据到缓冲区:阻塞等待客户端发送数据  
       // 返回值n: 实际读取到的字节数;err: 读取过程中的错误(如客户端断开连接)  
       n, err := reader.Read(buf[:]) // buf[:]将数组转为切片,满足Read函数的参数要求  
       if err != nil {  
          // 读取失败(如客户端主动断开连接),打印日志后跳出循环,结束当前客户端的处理  
          log.Println("读取客户端数据失败:", err)  
          break  
       }  
  
       // 4. 将读取到的字节数据转为字符串(方便打印和处理)  
       v := string(buf[:n]) // 只取前n个字节(避免缓冲区剩余的空字符干扰)  
  
       // 5. 打印客户端发送的数据(包含客户端地址,方便调试)  
       log.Printf("接收到客户端[%s]的信息:%s \n", conn.RemoteAddr(), v)  
  
       // 6. 将收到的数据原封不动回传给客户端(实现"回声服务":客户端发什么,服务器回什么)  
       // buf[:n]:只发送实际读取到的n个字节,避免发送缓冲区中的空字符  
       _, err = conn.Write(buf[:n])  
       if err != nil {  
          log.Println("向客户端回复数据失败:", err)  
          break  
       }  
    }  
}

客户端

  • 建立和服务端的连接
  • 收发消息
  • 关闭连接
package main  
  
import (  
    "bufio"  
    "log"
    "net"    
    "os"    
    "strings"
    )  
  
func main() {  
    // 建立与服务器的TCP连接(目标:本地20000端口)  
    conn, err := net.Dial("tcp", "127.0.0.1:20000")  
    if err != nil {  
       log.Fatal("连接服务器失败: ", err) // 连接失败直接终止(比如服务器没启动)  
    }  
    defer conn.Close() // 程序退出前自动关闭连接,避免资源泄漏  
  
    // 创建终端输入读取器(读取用户在控制台输入的内容)  
    input := bufio.NewReader(os.Stdin)  
    log.Println("客户端启动成功,输入消息发送给服务器(输入Q退出):")  
  
    for {  
       // 1. 读取终端输入(直到用户按下回车键,包含\n)  
       readString, err := input.ReadString('\n')  
       if err != nil {  
          log.Printf("读取输入失败: %v", err)  
          break  
       }  
  
       // 2. 处理输入:去掉首尾的换行符(\r\n),避免多余字符  
       inputInfo := strings.Trim(readString, "\r\n")  
  
       // 3. 输入Q(不区分大小写)则退出循环,关闭连接  
       if strings.ToUpper(inputInfo) == "Q" {  
          log.Println("客户端主动退出")  
          break  
       }  
  
       // 4. 发送处理后的有效数据给服务器  
       _, err = conn.Write([]byte(inputInfo))  
       if err != nil {  
          log.Printf("发送数据失败: %v", err)  
          break  
       }  
  
       // 5. 读取服务器的回复(最多读1024字节)  
       var buf [1024]byte  
       n, err := conn.Read(buf[:])  
       if err != nil {  
          log.Printf("与服务器通信异常: %v", err)  
          break  
       }  
  
       // 6. 打印服务器的有效回复(只取实际读取到的n字节)  
       log.Printf("客户端接受到的内容: %s", string(buf[:n]))  
    }  
}

检验

image.png

image.png