Go语言之网络编程,使用编程语言实现多台计算机的通信。

1、网络三要素

网络编程三要素:

(1)IP地址:网络中每一台计算机的唯一标识,通过IP地址找到指定的计算机。

(2)端口:用于标识进程的逻辑地址,通过端口找到指定进程。

(3)协议:定义通信规则,符合协议则可以通信,不符合不能通信。一般有TCP协议和UDP协议。

(1)IP地址

计算机分布在世界各地,要想和它们通信,必须要知道确切的位置。确定计算机位置的方式有多种,IP 地址是最常用的,例如,114.114.114.114 是国内第一个、全球第三个开放的 DNS 服务地址,127.0.0.1 是本机地址。
其实,我们的计算机并不知道 IP 地址对应的地理位置,当要通信时,只是将 IP 地址封装到要发送的数据包中,交给路由器去处理。路由器有非常智能和高效的算法,很快就会找到目标计算机,并将数据包传递给它,完成一次单向通信。
目前大部分软件使用 IPv4 地址,但 IPv6 也正在被人们接受,尤其是在教育网中,已经大量使用。

(2)端口

有了 IP 地址,虽然可以找到目标计算机,但仍然不能进行通信。一台计算机可以同时提供多种网络服务,例如Web服务、FTP服务(文件传输服务)、SMTP服务(邮箱服务)等,仅有 IP 地址,计算机虽然可以正确接收到数据包,但是却不知道要将数据包交给哪个网络程序来处理,所以通信失败。

为了区分不同的网络程序,计算机会为每个网络程序分配一个独一无二的端口号(Port Number),例如,Web服务的端口号是 80,FTP 服务的端口号是 21,SMTP 服务的端口号是 25。

端口(Port)是一个虚拟的、逻辑上的概念。可以将端口理解为一道门,数据通过这道门流入流出,每道门有不同的编号,就是端口号。如下图所示:
在这里插入图片描述

(3)协议

协议(Protocol)就是网络通信的约定,通信的双方必须都遵守才能正常收发数据。协议有很多种,例如 TCP、UDP、IP 等,通信的双方必须使用同一协议才能通信。协议是一种规范,由计算机组织制定,规定了很多细节,例如,如何建立连接,如何相互识别等。

协议仅仅是一种规范,必须由计算机软件来实现。例如 IP 协议规定了如何找到目标计算机,那么各个开发商在开发自己的软件时就必须遵守该协议,不能另起炉灶。

所谓协议族(Protocol Family),就是一组协议(多个协议)的统称。最常用的是 TCP/IP 协议族,它包含了 TCP、IP、UDP、Telnet、FTP、SMTP 等上百个互为关联的协议,由于 TCP、IP 是两种常用的底层协议,所以把它们统称为 TCP/IP 协议族。

(4)数据传输方式

计算机之间有很多数据传输方式,各有优缺点,常用的有两种:SOCK_STREAM 和 SOCK_DGRAM。

SOCK_STREAM 表示面向连接的数据传输方式。数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送,但效率相对较慢。常见的 http 协议就使用 SOCK_STREAM 传输数据,因为要确保数据的正确性,否则网页不能正常解析。

SOCK_DGRAM 表示无连接的数据传输方式。计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。因为 SOCK_DGRAM 所做的校验工作少,所以效率比 SOCK_STREAM 高。

QQ 视频聊天和语音聊天就使用 SOCK_DGRAM 传输数据,因为首先要保证通信的效率,尽量减小延迟,而数据的正确性是次要的,即使丢失很小的一部分数据,视频和音频也可以正常解析,最多出现噪点或杂音,不会对通信质量有实质的影响。

注意:SOCK_DGRAM 没有想象中的糟糕,不会频繁的丢失数据,数据错误只是小概率事件。

有可能多种协议使用同一种数据传输方式,所以在 socket 编程中,需要同时指明数据传输方式和协议。
综上所述:IP地址和端口能够在广袤的互联网中定位到要通信的程序,协议和数据传输方式规定了如何传输数据,有了这些,两台计算机就可以通信了。

TCP协议

(1)OSI模型

如果你读过计算机专业,或者学习过网络通信,那你一定听说过 OSI 模型,它曾无数次让你头大。OSI 是 Open System Interconnection 的缩写,译为“开放式系统互联”。 OSI 模型把网络通信的工作分为 7 层,从下到上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。

这个网络模型究竟是干什么呢?简而言之就是进行数据封装的。

当另一台计算机接收到数据包时,会从网络接口层再一层一层往上传输,每传输一层就拆开一层包装,直到最后的应用层,就得到了最原始的数据,这才是程序要使用的数据。

在这里插入图片描述

(2)TCP报文格式

TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的通信协议,数据在传输前要建立连接,传输完毕后还要断开连接。

客户端在收发数据前要使用 connect() 函数和服务器建立连接。建立连接的目的是保证IP地址、端口、物理链路等正确无误,为数据的传输开辟通道。

TCP建立连接时要传输三个数据包,俗称三次握手(Three-way Handshaking)。可以形象的比喻为下面的对话:

[Shake 1] 套接字A:“你好,套接字B,我这里有数据要传送给你,建立连接吧。”
[Shake 2] 套接字B:“好的,我这边已准备就绪。”
[Shake 3] 套接字A:“谢谢你受理我的请求。”

在这里插入图片描述

序号:Seq(Sequence Number)序号占32位,用来标识从计算机A发送到计算机B的数据包的序号,计算机发送数据时对此进行标记。

确认号:Ack(Acknowledge Number)确认号占32位,客户端和服务器端都可以发送,Ack = Seq + 1。

标志位:每个标志位占用1Bit,共有6个,分别为 URG、ACK、PSH、RST、SYN、FIN,具体含义如下:

// URG:紧急指针(urgent pointer)有效。
// ACK:确认序号有效。
// PSH:接收方应该尽快将这个报文交给应用层。
// RST:重置连接。
// SYN:建立一个新连接。
// FIN:断开一个连接。

(3)TCP/IP三次握手

使用 connect() 建立连接时,客户端和服务器端会相互发送三个数据包,请看下图:
在这里插入图片描述

客户端调用 socket() 创建套接字后,因为没有建立连接,所以套接字处于CLOSED状态;服务器端调用 listen() 函数后,套接字进入LISTEN状态,开始监听客户端请求。这个时候,客户端开始发起请求:

当客户端调用 connect() 函数后,TCP协议会组建一个数据包,并设置 SYN 标志位,表示该数据包是用来建立同步连接的。同时生成一个随机数字 1000,填充“序号(Seq)”字段,表示该数据包的序号。完成这些工作,开始向服务器端发送数据包,客户端就进入了SYN-SEND状态。

服务器端收到数据包,检测到已经设置了 SYN 标志位,就知道这是客户端发来的建立连接的“请求包”。服务器端也会组建一个数据包,并设置 SYN 和 ACK 标志位,SYN 表示该数据包用来建立连接,ACK 用来确认收到了刚才客户端发送的数据包。 服务器生成一个随机数 2000,填充“序号(Seq)”字段。2000 和客户端数据包没有关系。服务器将客户端数据包序号(1000)加1,得到1001,并用这个数字填充“确认号(Ack)”字段。服务器将数据包发出,进入SYN-RECV状态。

客户端收到数据包,检测到已经设置了 SYN 和 ACK 标志位,就知道这是服务器发来的“确认包”。客户端会检测“确认号(Ack)”字段,看它的值是否为 1000+1,如果是就说明连接建立成功。接下来,客户端会继续组建数据包,并设置 ACK 标志位,表示客户端正确接收了服务器发来的“确认包”。同时,将刚才服务器发来的数据包序号(2000)加1,得到 2001,并用这个数字来填充“确认(Ack)”字段。客户端将数据包发出,进入ESTABLISED状态,表示连接已经成功建立。

服务器端收到数据包,检测到已经设置了 ACK 标志位,就知道这是客户端发来的“确认包”。服务器会检测“确认号(Ack)”字段,看它的值是否为 2000+1,如果是就说明连接建立成功,服务器进入ESTABLISED状态。至此,客户端和服务器都进入了ESTABLISED状态,连接建立成功,接下来就可以收发数据了。

注意:三次握手的关键是要确认对方收到了自己的数据包,这个目标就是通过“确认号(Ack)”字段实现的。计算机会记录下自己发送的数据包序号 Seq,待收到对方的数据包后,检测“确认号(Ack)”字段,看Ack = Seq + 1是否成立,如果成立说明对方正确收到了自己的数据包

(4)TCP/IP四次挥手

建立连接非常重要,它是数据正确传输的前提;断开连接同样重要,它让计算机释放不再使用的资源。如果连接不能正常断开,不仅会造成数据传输错误,还会导致套接字不能关闭,持续占用资源,如果并发量高,服务器压力堪忧。

建立连接需要三次握手,断开连接需要四次握手,可以形象的比喻为下面的对话:
[Shake 1] 套接字A:“任务处理完毕,我希望断开连接。”
[Shake 2] 套接字B:“哦,是吗?请稍等,我准备一下。”
等待片刻后……
[Shake 3] 套接字B:“我准备好了,可以断开连接了。”
[Shake 4] 套接字A:“好的,谢谢合作。”

下图演示了客户端主动断开连接的场景:
在这里插入图片描述

建立连接后,客户端和服务器都处于ESTABLISED状态。这时,客户端发起断开连接的请求:

客户端调用 close() 函数后,向服务器发送 FIN 数据包,进入FIN_WAIT_1状态。FIN 是 Finish 的缩写,表示完成任务需要断开连接。

服务器收到数据包后,检测到设置了 FIN 标志位,知道要断开连接,于是向客户端发送“确认包”,进入CLOSE_WAIT状态。注意:服务器收到请求后并不是立即断开连接,而是先向客户端发送“确认包”,告诉它我知道了,我需要准备一下才能断开连接。

客户端收到“确认包”后进入FIN_WAIT_2状态,等待服务器准备完毕后再次发送数据包。

等待片刻后,服务器准备完毕,可以断开连接,于是再主动向客户端发送 FIN 包,告诉它我准备好了,断开连接吧。然后进入LAST_ACK状态。

客户端收到服务器的 FIN 包后,再向服务器发送 ACK 包,告诉它你断开连接吧。然后进入TIME_WAIT状态。

服务器收到客户端的 ACK 包后,就断开连接,关闭套接字,进入CLOSED状态。

​注意:关于 TIME_WAIT 状态的说明

客户端最后一次发送 ACK包后进入 TIME_WAIT 状态,而不是直接进入 CLOSED 状态关闭连接,这是为什么呢?

/*
TCP 是面向连接的传输方式,必须保证数据能够正确到达目标机器,不能丢失或出错,而网络是不稳定的,随时可能会毁坏数据,所以机器A每次向机器B发送数据包后,都要求机器B”确认“,回传ACK包,告诉机器A我收到了,这样机器A才能知道数据传送成功了。
如果机器B没有回传ACK包,机器A会重新发送,直到机器B回传ACK包。
客户端最后一次向服务器回传ACK包时,有可能会因为网络问题导致服务器收不到,服务器会再次发送 FIN 包,如果这时客户端完全关闭了连接,那么服务器无论如何也收不到ACK包了,所以客户端需要等待片刻、确认对方收到ACK包后才能进入CLOSED状态。
那么,要等待多久呢?数据包在网络中是有生存时间的,超过这个时间还未到达目标主机就会被丢弃,并通知源主机。
这称为报文最大生存时间(MSL,Maximum Segment Lifetime)。
TIME_WAIT 要等待 2MSL 才会进入 CLOSED 状态。ACK 包到达服务器需要 MSL 时间,服务器重传 FIN 包也需要 MSL 时间,2MSL 是数据包往返的最大时间,如果 2MSL 后还未收到服务器重传的 FIN 包,就说明服务器已经收到了 ACK 包。
*/

socket介绍

什么是 socket?

socket 的原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。 我们把插头插到插座上就能从电网获得电力供应,同样,为了与远程计算机进行数据传输,需要连接到因特网,而 socket 就是用来连接到因特网的工具。

在这里插入图片描述

socket缓冲区与阻塞

1、socket缓冲区

每个 socket 被创建后,都会分配两个缓冲区,输入缓冲区和输出缓冲区。write()/send() 并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器。一旦将数据写入到缓冲区,函数就可以成功返回,不管它们有没有到达目标机器,也不管它们何时被发送到网络,这些都是TCP协议负责的事情。

TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况、当前线程是否空闲等诸多因素,不由程序员控制。read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取。

在这里插入图片描述
这些I/O缓冲区特性可整理如下:

I/O缓冲区在每个TCP套接字中单独存在;
I/O缓冲区在创建套接字时自动生成;
即使关闭套接字也会继续传送输出缓冲区中遗留的数据;
关闭套接字将丢失输入缓冲区中的数据。
输入输出缓冲区的默认大小一般都是 8K!

2、阻塞模式

对于TCP套接字(默认情况下),当使用send() 发送数据时:
(1) 首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 send() 会被阻塞(暂停执行),直到缓冲区中的数据被发 送到目标机器,腾出足够的空间,才唤醒 send() 函数继续写入数据。

(2) 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,send() 也会被阻塞,直到数据发送完毕缓冲区解锁, send() 才会被唤醒。

(3) 如果要写入的数据大于缓冲区的最大长度,那么将分批写入。

(4) 直到所有数据被写入缓冲区 send() 才能返回。

当使用recv() 读取数据时:
(1) 首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来。

(2) 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 recv() 函数再次读取。

(3) 直到读取到数据后 recv() 函数才会返回,否则就一直被阻塞。

TCP套接字默认情况下是阻塞模式,也是最常用的。当然你也可以更改为非阻塞模式,后续我们会讲解。

TCP的粘包问题

上节我们讲到了socket缓冲区和数据的传递过程,可以看到数据的接收和发送是无关的,read()/recv() 函数不管数据发送了多少次,都会尽可能多的接收数据。也就是说,read()/recv() 和 write()/send() 的执行次数可能不同。

例如,write()/send() 重复执行三次,每次都发送字符串"abc”,那么目标机器上的 read()/recv() 可能分三次接收,每次都接收"abc";也可能分两次接收,第一次接收"abcab",第二次接收"cabc";也可能一次就接收到字符串"abcabcabc"。

这就是数据的“粘包”问题,客户端发送的多个数据包被当做一个数据包接收。也称数据的无边界性,read()/recv() 函数不知道数据包的开始或结束标志(实际上也没有任何开始或结束标志),只把它们当做连续的数据流来处理。

基于Go的socket代码实现

聊天案例

服务端

package main

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

func main() {
    // 1.创建TCP服务端监听
    listenner, err := net.Listen("tcp", "0.0.0.0:8888")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer listenner.Close()
    // 2.服务端不断等待请求处理
    for {
        // 阻塞等待客户端连接
        conn, err := listenner.Accept()
        if err != nil {
            fmt.Println(err)
            continue
        }
        go ClientConn(conn)
    }
}

// 处理服务端逻辑
func ClientConn(conn net.Conn) {
    defer conn.Close()
    // 获取客户端地址
    ipAddr := conn.RemoteAddr().String()
    fmt.Println(ipAddr, "连接成功")
    // 缓冲区
    buf := make([]byte, 1024)
    for {
        // n是读取的长度
        // time.Sleep(time.Second*10)  // 粘包
        n, err := conn.Read(buf)
        if err != nil {
            fmt.Println(err)
            return
        }
        // 切出有效数据
        result := buf[:n]
        fmt.Printf("接收到数据,来自[%s]    [%d]:%s\n", ipAddr, n, string(result))
        // 接收到exit,退出连接
        if string(result) == "exit" {
            fmt.Println(ipAddr, "退出连接")
            return
        }
        // 回复客户端
        conn.Write([]byte(strings.ToUpper(string(result))))
    }
}

客户端

package main

import (
    "fmt"
    "net"
)

func main() {
    // 1.连接服务端
    conn, err := net.Dial("tcp", "127.0.0.1:8888")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer conn.Close()
    // 缓冲区

    for {
        buf := make([]byte, 1024)
        fmt.Printf("请输入发送的内容:")
        fmt.Scan(&buf)
        fmt.Printf("发送的内容:%s\n", string(buf))
        // 发送数据
        conn.Write(buf)
        //conn.Write(buf)   // 粘包
        //conn.Write(buf)   // 粘包
        // 接收服务端返回信息
        res := make([]byte, 1024)
        n, err := conn.Read(res)
        if err != nil {
            fmt.Println(err)
            return
        }
        result := res[:n]
        fmt.Printf("接收到数据:%s\n", string(result))
    }
}

ssh案例

服务端

package main

import (
    "fmt"
    "github.com/axgle/mahonia"
    "net"
    "os/exec"
)

func main() {
    // 1.创建TCP服务端监听
    listenner, err := net.Listen("tcp", "0.0.0.0:8888")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer listenner.Close()

    for true {
        // 阻塞等待客户端连接
        conn, err := listenner.Accept()
        if err != nil {
            fmt.Println(err)
            return
        }
        go ClientConn(conn)
    }

}

// 处理服务端逻辑
func ClientConn(conn net.Conn) {
    defer conn.Close()
    // 获取客户端地址
    ipAddr := conn.RemoteAddr().String()
    fmt.Println(ipAddr, "连接成功")
    for true {
        // 缓冲区
        data := make([]byte, 1024)
        // n是读取的长度
        n, err := conn.Read(data)
        fmt.Println("命令字节数",n)
        if err != nil {
            fmt.Println(err)
            return
        }
        // 切出有效数据
        data = data[:n]
        fmt.Printf("接收到命令,来自[%s]    [%d]:%s\n", ipAddr, n, string(data))
        // 接收到exit,退出连接
        if string(data) == "exit" {
            fmt.Println(ipAddr, "退出连接")
            return
        }
        // 回复客户端
        cmd := exec.Command("cmd","/C",string(data))
        // 执行命令,并返回结果
        output,err := cmd.Output()
        if err != nil {
            panic(err)
        }

        fmt.Println("命令结果字节数:",len(output))
        dec := mahonia.NewDecoder("gbk")
        _, cdata, _ := dec.Translate(output, true)
        result := string(cdata)
        fmt.Println(result)
        conn.Write([]byte(string(result)))
    }

}

客户端

package main

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

func main() {
    // 1.连接服务端
    conn, err := net.Dial("tcp", "127.0.0.1:8888")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer conn.Close()
    // 缓冲区
    for true {

        reader := bufio.NewReader(os.Stdin) // 从标准输入生成读对象
        fmt.Println("输入执行命令>>>")
        text, _ := reader.ReadString('\n') // 读到换行
        text = strings.TrimSpace(text)

        fmt.Println("text",text)
        // 发送数据
        conn.Write([]byte(text))
        // 接收服务端返回信息
        res := make([]byte, 100000)  // 如何解决大文件传输问题呢?看下面的文件上传案例
        n, err := conn.Read(res)
        if err != nil {
            fmt.Println("err:",err)
            return
        }
        fmt.Println("n",n)
        result := res[:n]
        fmt.Printf("接收到数据:%s\n", string(result))
    }

}

上传文件案例

服务端

package main

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

func main() {
    // 1.创建TCP服务端监听
    listenner, err := net.Listen("tcp", "0.0.0.0:8888")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer listenner.Close()

    for true {
        // 阻塞等待客户端连接
        conn, err := listenner.Accept()
        if err != nil {
            fmt.Println(err)
            return
        }
        go ClientConn(conn)
    }

}

// 处理服务端逻辑
func ClientConn(conn net.Conn) {
    defer conn.Close()
    // 获取客户端地址
    ipAddr := conn.RemoteAddr().String()
    fmt.Println(ipAddr, "连接成功")
    for true {
        // 缓冲区
        infoByte := make([]byte, 1024)
        // n是读取的长度
        n, err := conn.Read(infoByte)
        if err != nil {
            fmt.Println(err)
            return
        }
        //
        nameAndSize := strings.Split(string(infoByte[:n])," ")
        fileSize, err := strconv.Atoi(nameAndSize[1])
        fileName := nameAndSize[0]
        // 读数据和写文件
        file, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
        writer := bufio.NewWriter(file)

        // 循环接收数据
        var readSize = 0
        fmt.Println(readSize, fileSize)
        for readSize < fileSize {
            data := make([]byte, 1024)
            // n是读取的长度
            n, _ := conn.Read(data)
            fmt.Println("n", n)
            _, _ = writer.WriteString(string(data[:n]))
            readSize += len(data)
        }
        _ = writer.Flush()
        fmt.Println("上传成功!")

    }

}

客户端

package main

import (
    "bufio"
    "fmt"
    "io"
    "net"
    "os"
    "strconv"
    "strings"
)

func main() {
    // 1.连接服务端
    conn, err := net.Dial("tcp", "127.0.0.1:8888")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer conn.Close()
    // 缓冲区
    for true {

        reader := bufio.NewReader(os.Stdin) // 从标准输入生成读对象
        fmt.Println("输入执行命令>>>")
        text, _ := reader.ReadString('\n') // 读到换行
        text = strings.TrimSpace(text)

        path := strings.Split(text," ")[1]

        //打开文件
        file, _ := os.Open(path)
        // 获取文件大小
        reader = bufio.NewReader(file)
        f, _ := os.Stat(path)

        // 发送文件大小
        fsize := f.Size()
        // 获取文件名称
        fname := f.Name()

        strInt64 := strconv.FormatInt(fsize, 10)
        _, _ = conn.Write([]byte(fname+" "+strInt64))

        // 发送文件数据
        for {
            // (1) 按行都字符串
            bytes, err := reader.ReadBytes('\n') // 读取到换行符为止,读取内容包括换行符
            // 发送数据
            _, _ = conn.Write(bytes)
            if err == io.EOF { //io.EOF 读取到了文件的末尾
                // fmt.Println("读取到文件末尾!")
                break
            }
        }

        fmt.Println("上传成功")

    }

}

web开发,http协议

HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于万维网(WWW:World Wide Web )服务器与本地浏览器之间传输超文本的传送协议。HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展。HTTP协议工作于客户端-服务端架构为上。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。Web服务器根据接收到的请求后,向客户端发送响应信息。

在这里插入图片描述

http协议特性

(1) 基于TCP/IP协议
http协议是基于TCP/IP协议之上的应用层协议。

(2) 基于请求-响应模式
HTTP协议规定,请求从客户端发出,最后服务器端响应该请求并 返回。换句话说,肯定是先从客户端开始建立通信的,服务器端在没有 接收到请求之前不会发送响应

在这里插入图片描述
(3) 无状态保存
HTTP是一种不保存状态,即无状态(stateless)协议。HTTP协议 自身不对请求和响应之间的通信状态进行保存。也就是说在HTTP这个 级别,协议对于发送过的请求或响应都不做持久化处理。

使用HTTP协议,每当有新的请求发送时,就会有对应的新响应产 生。协议本身并不保留之前一切的请求或响应报文的信息。这是为了更快地处理大量事务,确保协议的可伸缩性,而特意把HTTP协议设计成 如此简单的。

可是,随着Web的不断发展,因无状态而导致业务处理变得棘手 的情况增多了。比如,用户登录到一家购物网站,即使他跳转到该站的 其他页面后,也需要能继续保持登录状态。针对这个实例,网站为了能 够掌握是谁送出的请求,需要保存用户的状态。HTTP/1.1虽然是无状态协议,但为了实现期望的保持状态功能, 于是引入了Cookie技术。有了Cookie再用HTTP协议通信,就可以管 理状态了。有关Cookie的详细内容稍后讲解。

在这里插入图片描述
(4) 无连接
无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。

(3)14.1.3 、http请求协议与响应协议

http协议包含由浏览器发送数据到服务器需要遵循的请求协议与服务器发送数据到浏览器需要遵循的请求协议。用于HTTP协议交互的信被为HTTP报文。请求端(客户端)的HTTP报文 做请求报文,响应端(服务器端)的 做响应报文。HTTP报文本身是由多行数据构成的字文本。

(1) 请求协议

在这里插入图片描述
请求方式: get与post请求

GET提交的数据会放在URL之后,以?分割URL和传输数据,参数之间以&相连,如EditBook?name=test1&id=123456. POST方法是把提交的数据放在HTTP包的请求体中.
GET提交的数据大小有限制(因为浏览器对URL的长度有限制),而POST方法提交的数据没有限制

(2) 响应协议
在这里插入图片描述
响应状态码:状态码的职 是当客户端向服务器端发送请求时, 返回的请求 结果。借助状态码,用户可以知道服务器端是正常 理了请求,还是出 现了 。状态码如200 OK,以3位数字和原因 成。数字中的 一位指定了响应 别,后两位无分 。响应 别有以5种。

在这里插入图片描述

基于net库的web应用

package main

import (
    "fmt"
    "net"
)

func main()  {

    listenner, err := net.Listen("tcp", "0.0.0.0:8888")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer listenner.Close()
    // 2.服务端不断等待请求处理
    for {
        // 阻塞等待客户端连接
        conn, err := listenner.Accept()
        if err != nil {
            fmt.Println(err)
            continue
        }
        buf := make([]byte, 1024)
        n, err := conn.Read(buf)
        fmt.Println("n",n)
        conn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n<h1>Welcome to Web World!</h1>"))
    }

}

基于http库的web应用

package main

import (
    "fmt"
    "net/http"
)

func foo (w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello Yuan")
}

func main() {

    http.HandleFunc("/hi", foo)
    http.ListenAndServe(":8090", nil)
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值