1.模型分层
基本上大学的计算机基础都有学过,7层就是物数网传会表应,5层就是把会表应合并为应用层
1.1Socket抽象层
两个进程进行通信的前提:
- 本地:只需要知道进程唯一id-pid(本地进程唯一标识符)即可
- 网络:IP地址+协议+端口号可以确定网络中的一个进程
回想一下之前的GMP模型中的P也是抽象出来帮助M调度G的,那我们利用Socket主要的操作是帮助应用层建立、接受连接,读写“文件”、关闭接口、超时,还要获取对方的地址和端口。
Socket起源于UNIX,在Unix一切皆文件哲学的思想下,Socket是一种"打开—读/写—关闭"模式的实现,服务器和客户端各自维护一个"文件",在建立连接打开后,可以向自己文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。
2.TCP连接建立
2.1TCP服务端准备连接过程
我们需要使用socket套接字来帮助我们建立连接,在C语言中
int socket(int domain, int type, int protocol)
//domain指PF_INET、PF_INET6等表示是什么样的套接字
//type指SOCK_STREAM(tcp)/SOCK_DGRAM(UDP)/SOCK_RAM(原始套接字)
//最后一个被废弃为0
//返回的int就是套接字的文件句柄
我们知道在网络中确定一个进程需要ip和port,我们将我们的信息传入socket,让别让来发现我们的服务
bind(int fd, sockaddr * addr, socklen_t len)
一个人建立连接是不可能的,我们需要等待其他人和我们建立连接
int listen (int socketfd, int backlog)
注意第二个参数backlog在Linux中表示已完成(ESTABLISHED)并且未accept的队列大小,即可以接受的并发数目。
此时listen只是让套接字从“主动”转为“被动”状态,告诉内核这个套接字是用来等待连接请求的,并未开始真正等待,需要调用accept函数
int accept(int listensockfd, struct sockaddr *cliaddr, socklen_t *addrlen)
我们能通过这个函数获取到对方的地址(存入cliaddr,addrlen为地址大小)
监听套接字是阻塞的是一直存在的
2.2TCP客户端发起连接请求
bind之后直接connect
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)
调用connect函数触发TCP三次握手
2.3利用GO搭建一个简单的TCP服务器和客户端,只需listen即可完成bind和listen
server.go
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
"time"
)
func main() {
arguments := os.Args
if len(arguments) == 1 {
fmt.Println("Please provide port number")
return
}
PORT := ":" + arguments[1]
l, err := net.Listen("tcp", PORT)
if err != nil {
fmt.Println(err)
return
}
defer l.Close()
c, err := l.Accept()
if err != nil {
fmt.Println(err)
return
}
for {
netData, err := bufio.NewReader(c).ReadString('\n')
if err != nil {
fmt.Println(err)
return
}
if strings.TrimSpace(string(netData)) == "STOP" {
fmt.Println("Exiting TCP server!")
return
}
fmt.Print("-> ", string(netData))
t := time.Now()
myTime := t.Format(time.RFC3339) + "\n"
c.Write([]byte(myTime))
}
}
服务端使用Dial函数完成bind和connect
client.go
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
func main() {
arguments := os.Args
if len(arguments) == 1 {
fmt.Println("Please provide host:port.")
return
}
CONNECT := arguments[1]
c, err := net.Dial("tcp", CONNECT)
if err != nil {
fmt.Println(err)
return
}
for {
reader := bufio.NewReader(os.Stdin)
fmt.Print(">> ")
text, _ := reader.ReadString('\n')
fmt.Fprintf(c, text+"\n")
message, _ := bufio.NewReader(c).ReadString('\n')
fmt.Print("->: " + message)
if strings.TrimSpace(string(text)) == "STOP" {
fmt.Println("TCP client exiting...")
return
}
}
}
两者编译后使用tcpdump抓包(注意我这是一台机上使用环回网络)
pts1执行命令
tcpdump -i lo port 8080
pts2启动server
./server 8080
pts3 启动客户端并输入STOP
./client 127.0.0.1:8080
STOP
观察tcpdump执行结果
22:08:21.107664 IP localhost.50442 > localhost.webcache: Flags [S], seq 2024884550, win 43690, options [mss 65495,sackOK,TS val 1340202011 ecr 0,nop,wscale 7], length 0
22:08:21.107673 IP localhost.webcache > localhost.50442: Flags [S.], seq 607092787, ack 2024884551, win 43690, options [mss 65495,sackOK,TS val 1340202011 ecr 1340202011,nop,wscale 7], length 0
22:08:21.107680 IP localhost.50442 > localhost.webcache: Flags [.], ack 607092788, win 342, options [nop,nop,TS val 1340202011 ecr 1340202011], length 0
这是三次握手过程,首先客户端发起建立请求
1.客户端第一次握手:
localhost.50424(client随机占用端口)->localhost.webcache
Flags [S],S:SYN标志位表示同步,开始会话请求
seq 2024884550表示发送了这么多字节的数据
2.服务端第二次握手
localhost.webcache > localhost.50424
Flags [S.] 点实际表示ACK确认,这是对SYN的确认
seq 607092787, ack 2024884551
这里我们看到ack对上一个包的seq序号+1确认
3.客户端第三次握手
localhost.50442 > localhost.webcache
Flags [.], ack 607092788
对上一个包的序号+1
至此连接完成
(这个demo例子客户端的断开没写,因此不分析四次挥手了)