共计 9109 个字符,预计需要花费 23 分钟才能阅读完成。
本篇内容介绍了“linux socket 怎么使用”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让丸趣 TV 小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!
socket 又称套接字,是 Linux 跨进程通信(IPC)方式的一种,它不仅仅可以做到同一台主机内跨进程通信,还可以做到不同主机间的跨进程通信。
本教程操作环境:linux5.9.8 系统、Dell G3 电脑。
socket 的原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。
linux 中的 socket
Socket 是 Linux 跨进程通信(IPC,Inter Process Communication,详情参考:Linux 进程间通信方式总结)方式的一种。相比于其他 IPC 方式,Socket 更牛的地方在于,它不仅仅可以做到同一台主机内跨进程通信,它还可以做到不同主机间的跨进程通信。根据通信域的不同可以划分成 2 种:Unix domain socket 和 Internet domain socket。
1. Internet domain socket
Internet domain socket 用于实现不同主机上的进程间通信,大部分情况下我们所说的 socket 都是指 internet domain socket。(下文不特殊指代的情况下,socket 就是指 internet domain socket。)
要做到不同主机跨进程通信,第一个要解决的问题就是怎么唯一标识一个进程。我们知道主机上每个进程都有一个唯一的 pid,通过 pid 可以解决同一台主机上的跨进程通信进程的识别问题。但是如果 2 个进程不在一台主机上的话,pid 是有可能重复的,所以在这个场景下不适用,那有什么其他的方式吗?我们知道通过主机 IP 可以唯一锁定主机,而通过端口可以定位到程序,而进程间通信我们还需要知道通信用的什么协议。这样一来“IP+ 端口 + 协议”的组合就可以唯一标识网络中一台主机上的一个进程。这也是生成 socket 的主要参数。
每个进程都有唯一标识之后,接下来就是通信了。通信这事一个巴掌拍不响,有发送端程序就有接收端程序,而 Socket 可以看成在两端进行通讯连接中的一个端点,发送端将一段信息写入发送端 Socket 中,发送端 Socket 将这段信息发送给接收端 Socket,最后这段信息传送到接收端。至于信息怎么从发送端 Socket 到接收端 Socket 就是操作系统和网络栈该操心的事情,我们可以不用了解细节。如下图所示:
为了维护两端的连接,我们的 Socket 光有自己的唯一标识还不够,还需要对方的唯一标识,所以一个上面说的发送端和接收端 Socket 其实都只有一半,一个完整的 Socket 的组成应该是由[协议,本地地址,本地端口,远程地址,远程端口] 组成的一个 5 维数组。比如发送端的 Socket 就是 [tcp,发送端 IP,发送端 port,接收端 IP,接收端 port],那么接收端的 Socket 就是 [tcp,接收端 IP,接收端 port,发送端 IP,发送端 port]。
打个比方加深下理解,就比如我给你发微信联系你这个场景,我俩就是进程,微信客户端就是 Socket,微信号就是我俩的唯一标识,至于腾讯是怎么把我发的微信消息传到你的微信上的细节,我们都不需要关心。为了维持我俩的联系,我们的 Socket 光有微信客户端还不行,我俩还得加好友,这样通过好友列表就能互相找到,我的微信客户端的好友列表中的你就是我的完整 Socket,而你的微信客户端的好友列表中的我就是你的完整 Socket。希望没有把你们弄晕。。。
Socket 根据通信协议的不同还可以分为 3 种:流式套接字 (SOCK_STREAM),数据报套接字(SOCK_DGRAM) 及原始套接字。
流式套接字(SOCK_STREAM):最常见的套接字,使用 TCP 协议,提供可靠的、面向连接的通信流。保证数据传输是正确的,并且是顺序的。应用于 Telnet 远程连接、WWW 服务等。
数据报套接字(SOCK_DGRAM):使用 UDP 协议,提供无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠性。使用 UDP 的应用程序要有自己的对数据进行确认的协议。
原始套接字:允许对低层协议如 IP 或 ICMP 直接访问,主要用于新的网络协议实现的测试等。原始套接字主要用于一些协议的开发,可以进行比较底层的操作。它功能强大,但是没有上面介绍的两种套接字使用方便,一般的程序也涉及不到原始套接字。
套接字工作过程如下图所示(以流式套接字为例,数据报套接字流程有所不同,可以参考:什么是套接字(Socket)):服务器首先启动,通过调用 socket()建立一个套接字,然后调用 bind()将该套接字和本地网络地址联系在一起,再调用 listen()使套接字做好侦听的准备,并规定它的请求队列的长度,之后就调用 accept()来接收连接。客户端在建立套接字后就可调用 connect()和服务器建立连接。连接一旦建立,客户机和服务器之间就可以通过调用 read()和 write()来发送和接收数据。最后,待数据传送结束后,双方调用 close()关闭套接字。
从 TCP 连接视角看待上述过程可以总结如图,可以看到 TCP 的三次握手代表着 Socket 连接建立的过程,建立完连接后就可以通过 read,wirte 相互传输数据,最后四次挥手断开连接删除 Socket。
2. Unix domain socket
Unix domain socket 又叫 IPC(inter-process communication 进程间通信) socket,用于实现同一主机上的进程间通信。socket 原本是为网络通讯设计的,但后来在 socket 的框架上发展出一种 IPC 机制,就是 UNIX domain socket。虽然网络 socket 也可用于同一台主机的进程间通讯(通过 loopback 地址 127.0.0.1),但是 UNIX domain socket 用于 IPC 更有效率:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。这是因为,IPC 机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的。
UNIX domain socket 是全双工的,API 接口语义丰富,相比其它 IPC 机制有明显的优越性,目前已成为使用最广泛的 IPC 机制,比如 X Window 服务器和 GUI 程序之间就是通过 UNIX domain socket 通讯的。Unix domain socket 是 POSIX 标准中的一个组件,所以不要被名字迷惑,linux 系统也是支持它的。
了解 Docker 的同学应该知道 Docker daemon 监听一个 docker.sock 文件,这个 docker.sock 文件的默认路径是 /var/run/docker.sock,这个 Socket 就是一个 Unix domain socket。在后面的实践环节会详细介绍。
Socket 实践
要学好编程,最好的方式就是实践。接下来我们来实际用下 Socket 通信,并且观察 Socket 文件
1. Internet domain socket 实践
现在我们就用 socket 写一个 server,由于本人 C 语言经验较少,所以这里我选择用 GoLang 实践。server 的功能很简单,就是监听 1208 端口,当收到输入 ping 时就返回 pong,收到 echo xxx 就返回 xxx,收到 quit 就关闭连接。socket-server.go 的代码参考文章:使用 Go 进行 Socket 编程 | 始于珞尘。如下:
package main
import (
fmt
net
strings
func connHandler(c net.Conn) {
if c == nil {
return
buf := make([]byte, 4096)
for {cnt, err := c.Read(buf)
if err != nil || cnt == 0 {c.Close()
break
inStr := strings.TrimSpace(string(buf[0:cnt]))
inputs := strings.Split(inStr, )
switch inputs[0] {
case ping :
c.Write([]byte( pong\n))
case echo :
echoStr := strings.Join(inputs[1:], ) + \n
c.Write([]byte(echoStr))
case quit :
c.Close()
break
default:
fmt.Printf(Unsupported command: %s\n , inputs[0])
fmt.Printf(Connection from %v closed. \n , c.RemoteAddr())
func main() {server, err := net.Listen( tcp , :1208)
if err != nil {fmt.Printf( Fail to start server, %s\n , err)
fmt.Println(Server Started ...)
for {conn, err := server.Accept()
if err != nil {fmt.Printf( Fail to connect, %s\n , err)
break
go connHandler(conn)
}
在一切皆文件的 Unix-like 系统中,进程生产的 socket 通过 socket 文件来表示,进程通过向 socket 文件读写内容实现消息的传递。在 Linux 系统中,通常 socket 文件在 /proc/pid/fd/ 文件路径下。启动我们的 socket-server,我们来窥探一下对应的 socket 文件。先启动 server:
# go run socket-server.go
Server Started ...
再开一个窗口,我们先查看 server 进程的 pid,可以使用 lsof 或 netstat 命令:
# lsof -i :1208
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
socket-se 20007 root 3u IPv6 470314 0t0 TCP *:1208 (LISTEN)
# netstat -tupan | grep 1208
tcp6 0 0 :::1208 :::* LISTEN 20007/socket-server
可以看到我们的 server pid 为 20007,接下来我们来查看下 server 监听的 socket:
# ls -l /proc/20007/fd
total 0
lrwx------ 1 root root 64 Sep 11 07:15 0 - /dev/pts/0
lrwx------ 1 root root 64 Sep 11 07:15 1 - /dev/pts/0
lrwx------ 1 root root 64 Sep 11 07:15 2 - /dev/pts/0
lrwx------ 1 root root 64 Sep 11 07:15 3 - socket:[470314]
lrwx------ 1 root root 64 Sep 11 07:15 4 - anon_inode:[eventpoll]
可以看到 /proc/20007/fd/ 3 是一个链接文件,指向 socket:[470314],这个便是 server 端的 socket。socket-server 启动经历了 socket() — bind() — listen()3 个过程,创建了这个 LISTEN socket 用来监听对 1208 端口的连接请求。
我们知道 socket 通信需要一对 socket:server 端和 client 端。现在我们再开一个窗口,在 socket-server 的同一台机器上用 telnet 启动一个 client,来看看 client 端的 socket:
# telnet localhost 1208
Trying 127.0.0.1...
Connected to localhost.
Escape character is ^] .
继续查看 server 端口打开的文件描述符;
# lsof -i :1208
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
socket-se 20007 root 3u IPv6 470314 0t0 TCP *:1208 (LISTEN)
socket-se 20007 root 5u IPv6 473748 0t0 TCP localhost:1208- localhost:51090 (ESTABLISHED)
telnet 20375 ubuntu 3u IPv4 473747 0t0 TCP localhost:51090- localhost:1208 (ESTABLISHED)
我们发现,相对于之前的结果多了 2 条,这 3 条分别是:
*:1208 (LISTEN)是 server 到监听 socket 文件名,所属进程 pid 是 20007
localhost:1208- localhost:51090 (ESTABLISHED)是 server 端为 client 端建立的新的 socket,负责和 client 通信,所属进程 pid 是 20007
localhost:51090- localhost:1208 (ESTABLISHED)是 client 端为 server 端建立的新的 socket,负责和 server 通信,所属进程 pid 是 20375
在 /proc/pid/fd/ 文件路径下可以看到 server 和 client 新建的 socket,这里不做赘述。从第 3 条结果我们可以看出,前 2 条 socket,LISTEN socket 和新建的 ESTABLISHED socket 都属于 server 进程,对于每条链接 server 进程都会创建一个新的 socket 去链接 client,这条 socket 的源 IP 和源端口为 server 的 IP 和端口,目的 IP 和目的端口是 client 的 IP 和端口。相应的 client 也创建一条新的 socket,该 socket 的源 IP 和源端口与目的 IP 和目的端口恰好与 server 创建的 socket 相反,client 的端口为一个主机随机分配的高位端口。
从上面的结果我们可以回答一个问题“服务端 socket.accept 后, 会产生新端口吗”? 答案是不会。server 的监听端口不会变,server 为 client 创建的新的 socket 的端口也不会变,在本例中都是 1208。这难到不会出现端口冲突吗?当然不会,我们知道 socket 是通过 5 维数组 [协议,本地 IP,本地端口,远程 IP,远程端口] 来唯一确定的。socket: *:1208 (LISTEN) 和 socket: localhost:1208- localhost:51090 (ESTABLISHED)是不同的 socket。那这个 LISTEN socket 有什么用呢?我的理解是当收到请求连接的数据包,比如 TCP 的 SYN 请求,那么这个连接会被 LISTEN socket 接收,进行 accept 处理。如果是已经建立过连接后的客户端数据包,则将数据放入接收缓冲区。这样,当服务器端需要读取指定客户端的数据时,则可以利用 ESTABLISHED 套接字通过 recv 或者 read 函数到缓冲区里面去取指定的数据,这样就可以保证响应会发送到正确的客户端。
上面提到客户端主机会为发起连接的进程分配一个随机端口去创建一个 socket,而 server 的进程则会为每个连接创建一个新的 socket。因此对于客户端而言,由于端口最多只有 65535 个,其中还有 1024 个是不准用户程序用的,那么最多只能有 64512 个并发连接。对于服务端而言,并发连接的总量受到一个进程能够打开的文件句柄数的限制,因为 socket 也是文件的一种,每个 socket 都有一个文件描述符(FD,file descriptor),进程每创建一个 socket 都会打开一个文件句柄。该上限可以通过 ulimt - n 查看,通过增加 ulimit 可以增加 server 的并发连接上限。本例的 server 机器的 ulimit 为:
# ulimit -n
1024
上面讲了半天服务端与客户端的 socket 创建,现在我们来看看服务端与客户端的 socket 通信。还记得我们的 server 可以响应 3 个命令吗,分别是 ping,echo 和 quit,我们来试试:
# telnet localhost 1208
Trying 127.0.0.1...
Connected to localhost.
Escape character is ^] .
echo Hello,socket
Hello,socket
Connection closed by foreign host.
我们可以看到 client 与 server 通过 socket 的通信。
到此为止,我们来总结下从 telnet 发起连接,到客户端发出 ping,服务端响应 pong,到最后客户端 quit,连接断开的整个过程:
telnet 发起向 localhost:1208 发起连接请求;
server 通过 socket: TCP *:1208 (LISTEN)收到请求数据包,进行 accept 处理;
server 返回 socket 信息给客户端,客户端收到 server socket 信息,为客户端进程分配一个随机端口 51090,然后创建 socket: TCP localhost:51090- localhost:1208 来连接服务端;
服务端进程创建一个新的 socket: TCP localhost:1208- localhost:51090 来连接客户端;
客户端发出 ping,ping 数据包 send 到 socket: TCP localhost:51090- localhost:1208;
服务端通过 socket: TCP localhost:1208- localhost:51090 收到 ping 数据包,返回 pong,pong 数据包又通过原路返回到客户端,完成一次通信。
客户端进程发起 quit 请求,通过上述相同的 socket 路径到达服务端后,服务端切断连接,服务端删除 socket: TCP localhost:1208- localhost:51090 释放文件句柄;客户端删除 socket: TCP localhost:51090- localhost:1208,释放端口 51090。
在上述过程中,socket 到 socket 之间还要经过操作系统,网络栈等过程,这里就不做细致描述。
2. Unix domain socket 实践
我们知道 docker 使用的是 client-server 架构,用户通过 docker client 输入命令,client 将命令转达给 docker daemon 去执行。docker daemon 会监听一个 unix domain socket 来与其他进程通信,默认路径为 /var/run/docker.sock。我们来看看这个文件:
# ls -l /var/run/docker.sock
srw-rw---- 1 root docker 0 Aug 31 01:19 /var/run/docker.sock
可以看到它的 Linux 文件类型是“s”,也就是 socket。通过这个 socket,我们可以直接调用 docker daemon 的 API 进行操作,接下来我们通过 docker.sock 调用 API 来运行一个 nginx 容器,相当于在 docker client 上执行:
# docker run nginx
与在 docker client 上一行命令搞定不同的是,通过 API 的形式运行容器需要 2 步:创建容器和启动容器。
1. 创建 nginx 容器,我们使用 curl 命令调用 docker API,通过 –unix-socket /var/run/docker.sock 指定 Unix domain socket。首先调用 /containers/create,并传入参数指定镜像为 nginx,如下:
# curl -XPOST --unix-socket /var/run/docker.sock -d {Image : nginx} -H Content-Type: application/json http://localhost/containers/create
{Id : 67bfc390d58f7ba9ac808d3fc948a5d4e29395e94288a7588ec3523af6806e1a , Warnings :[]}
2. 启动容器,通过上一步创建容器返回的容器 id,我们来启动这个 nginx:
# curl -XPOST –unix-socket /var/run/docker.sock http://localhost/containers/67bfc390d58f7ba9ac808d3fc948a5d4e29395e94288a7588ec3523af6806e1a/start
# docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
67bfc390d58f nginx /docker-entrypoint.… About a minute ago Up 7 seconds 80/tcp romantic_heisenberg
至此,通过 Unix domain socket 我们实现了客户端进程 curl 与服务端进程 docker daemon 间的通信,并成功地调用了 docker API 运行了一个 nginx container。
值得注意的是,在连接服务端的 Unix domain socket 的时候,我们直接指定的是服务端的 socket 文件。而在使用 Internet domain socket 的时候,我们指定的是服务端的 IP 地址和端口号。
“linux socket 怎么使用”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注丸趣 TV 网站,丸趣 TV 小编将为大家输出更多高质量的实用文章!