unix套接字编程
这是学习[1]Unix网络编程第一卷的一点笔记。
? 图1 基本TCP客户——服务器程序的流程图
首先启动服务器,稍后的某个时刻启动客户端,它要连接到服务器上。假设客户端给服务器发送了一个请求,服务器处理这个请求,并给客户端发回一个响应。这个过程一直持续下去,直到客户端给服务器发送一个文件结束符,并关闭客户端连接,接着服务器也关闭服务器端的连接,或结束运行或等待一个新的客户端连接。
socket函数
? 为了执行网络I/O,一个进程必须做的第一件事情就是调用socket函数,指定期望的通信协议类型。
#include <sys/socket.h>
int socket(int family, int type, int protocol);
族 | 解释 |
---|
AF_INET | IPv4协议 | AF_INET6 | IPv6协议 | AF_LOCAL | Unix域协议 | AF_ROUTE | 路由套接口 | AF_KEY | 密钥套接口 |
? 表1 socket函数的协议族(family)常值
类型 | 解释 |
---|
SOCK_STREAM | 字节流套接口 | SOCK_DGRAM | 数据包套接口 | SOCK_RAW | 原始套接口 |
? 表2 socket函数套接口类型(type)
| AF_INET | AF_INET6 | AF_LOCAL | AF_ROUTE | AF_KEY |
---|
SOCKET_STREAM | TCP | TCP | Yes | | | SOCK_DGRAM | UDP | UDP | Yes | | | SOCK_RAW | IPv4 | IPv6 | | Yes | Yes |
? 表3 socket函数的族与类型的组合
? 并非所有套接口family与type的组合都是有效的,表3给出了一些有效的组合和对应的真正协议。其中标"Yes"的项也是有效的,但还没有找到便捷的缩略词;而空白项则是不支持的。
? socket函数在成功时返回一个小的非负整数,它与文件描述符类似,我们把它称为套接字接口描述字(socket descriptor),简称套接字(sockfd)。为了得到这个套接口描述字,我们只是制定了协议族(IPv4,IPv6或Unix)和套接口类型(字节流、数据包或原始套接口)。我们并没有指定本地协议地址或远程协议地址。
connect函数
? TCP客户端用connect函数来建立一个与TCP服务器的连接
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr* servaddr, socklen_t addrlen);
bind函数
? 函数bind给套接口分配一个本地协议端口,对于网际协议,协议地址是32位IPv4地址或128位IPv6地址与16位的TCP或UDP端口号的组合。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* myaddr, socklen_t addrlen);
? 第二个参数是一个指向特定协议的地址结构的指针,第三个参数是该地址结构的长度。对于TCP,调用函数bind可以指定一个端口号,指定一个IP地址,可以两者都指定,也可以一个都不指定。
listen函数
函数listen仅被TCP服务器调用,它做两件事情:
1.当函数socket创建一个套接口时,它被假设为一个主动套接口,也就是说,它是一个将调用connect发起连接的客户套接口,函数listen将未连接的套接口转换成被动套接口,指示内核应接受指向此套接口的连接。根据TCP状态转换图,调用函数listen导致套接口从CLOSED状态转换到LISTEN状态。
2.函数的第二个参数规定了内核为此套接口排队的最大连接个数。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
accept函数
? accept由TCP服务器调用,从已完成连接队列头返回下一个已完成连接。若已完成连接队列为空,则进程睡眠。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr* cliaddr, socklen_t* addrlen);
? 参数cliaddr和addrlen用来返回连接对方进程的协议地址。addrlen是值-结果参数:调用前,我们将*addrlen所指的整数值置为由cliaddr所指的套接口地址结构的长度,返回时,此整数值即为由内核存在此套接口地址结构内的准确字节数。
? 如果函数accept执行成功,则返回值时内核自动生成的一个全新描述字,代表与客户的TCP连接。当我们讨论函数accept时,常把它的第一个参数称为监听套接口(listening socket)套接字(由函数socket生成的描述字,用作函数bind和listen的第一个参数),把它的返回值称为已连接套接口(connected socket)描述字。将这两个套接口区分开是很重要的。一个给定的服务器常常是只生成一个监听套接口且一直存在,直到该服务器关闭。内核为每个被接受的客户端连接创建一个已连接口(也就是说内核已为它完成TCP的三路握手过程)。当服务器完成某客户的服务时,关闭已连接套接口。
fork和exec函数
fork是unix中派生新进程的唯一方法
#include <unistd.h>
pid_t fork(void);
? fork在子进程返回0而不是父进程ID,一个原因是:子进程只有一个父进程,它总可以调用getppid来得到;而父进程有许多子进程,它没有办法来得到各子进程的ID。如果父进程想跟踪所有子进程的ID,它必须记住fork的返回值。我觉得还有一个就是fork在子进程中返回0使得子进程父进程的判断更加简单。
? fork有两个典型应用:
? 1.一个进程可以为自己创建一个拷贝,这样,当一个拷贝处理一个操作时,其他的拷贝可以执行其他的任务。这是非常典型的网络服务器。
? 2.一个进程想执行其他的程序,由于创建新进程的唯一方法是调用fork,进程首先调用fork来生成一个拷贝,然后其中一个拷贝(通常为子进程)调用exec来代替自己去执行新程序。
并发服务器
pid_t pid;
int listenfd, connfd;
listenfd = Socket(...);
Bind(listenfd, ...);
Listen(listenfd, LISTENQ);
for(;;) {
connfd = Accept(listenfd, ...);
if((pid==Fork())==0) {
Close(listenfd);
doit(connfd);
Close(connfd);
exit(0);
}
Close(connfd);
}
每个文件或套接口都有一个访问计数,该访问计数在文件表项中维护,它表示当前指向该文件或套接口的打开的描述字个数。
Close函数
? 一般Unix函数close也用来关闭套接口,终止TCP连接。
#include <unistd.h>
int close(int sockfd);
? TCP套接口的close其缺省功能是将套接口做上“已关闭”标记,并立即返回到进程。这个套接口描述字不能再为进程所用:它不能用作函数read或write的参数,但TCP将试着发送已排队待发的任何数据,然后按正常的TCP连接终止序列进行操作。
getsockname和getpeername函数
? 这两个函数或返回与套接口关联的本地协议地址(getsockname),或返回与套接口关联的远程协议地址(getpeername)。
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr* localaddr, socklen_t* addrlen);
int getpeername(int sockfd, struct sockaddr* peeraddr, socklen_t* addrlen);
一个使用TCP进行通讯的例子
例子来自[2]https://github.com/troydhanson/network
客户端
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
char *server = "127.0.0.1";
int port = 6180;
int main(int argc, char *argv[]) {
char *buf = "hello, world!";
int buflen = strlen(buf), rc;
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
printf("socket: %s\n", strerror(errno));
exit(-1);
}
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = inet_addr(server);
sin.sin_port = htons(port);
if (sin.sin_addr.s_addr == INADDR_NONE) {
printf("invalid remote IP %s\n", server);
exit(-1);
}
if (connect(fd, (struct sockaddr*)&sin, sizeof(sin)) == -1) {
printf("connect: %s\n", strerror(errno));
exit(-1);
}
if ( (rc=write(fd,buf,buflen)) != buflen) {
printf("write: %s\n", (rc<0)?strerror(errno):"incomplete");
exit(-1);
}
}
服务器
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define BUFSZ 200
int port = 6180;
int main(int argc, char *argv[]) {
char buf[BUFSZ];
int rc;
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
printf("socket: %s\n", strerror(errno));
exit(-1);
}
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = htonl(INADDR_ANY);
sin.sin_port = htons(port);
if (bind(fd, (struct sockaddr*)&sin, sizeof(sin)) == -1) {
printf("bind: %s\n", strerror(errno));
exit(-1);
}
if (listen(fd,1) == -1) {
printf("listen: %s\n", strerror(errno));
exit(-1);
}
while (1) {
struct sockaddr_in cin;
socklen_t cin_sz = sizeof(cin);
int fa = accept(fd, (struct sockaddr*)&cin, &cin_sz);
if (fa == -1) {
printf("accept: %s\n", strerror(errno));
continue;
}
if (sizeof(cin)==cin_sz) printf("connection from %s:%d\n",
inet_ntoa(cin.sin_addr), (int)ntohs(cin.sin_port));
do {
rc = read(fa,buf,BUFSZ);
if (rc==-1) printf("read: %s\n", strerror(errno));
else if (rc==0) printf("connection closed\n");
else printf("received %d bytes: %.*s\n", rc, rc, buf);
} while (rc > 0);
close(fa);
}
}
Unix域协议
? Unix域协议并不是一个实际的协议族,它只是在同一台主机上进行客户—服务器通信时,使用与在不同主机上的客户和服务器间通信时相同的API的一种方法。
? Unix域协议提供了两种类型的套接口:字节流套接口(与TCP类似)和数据包套接口(与UDP类似)。
为什么选择使用Unix域套接口?
? 1.在源自Berkeley的实现中,当通信双方位于同一台主机上时,Unix域套接口的速度通常是TCP套接口的两倍。
? 2.Unix域套接口可以用来在同一台主机上的各个进程之间传递描述字。
? 3.Unix域套接口的较新实现中可以向服务器提供客户的凭证(用户ID和组ID),这能提供附加的安全检查。
Unix域套接口地址结构
? Unix域套接口地址结构定义在<sys/un.h>中
struct sockaddr_un {
uint8_t sun_len;
sa_family_t sun_family;
char sun_path[104];
};
? sun_path数组中存放的路径名必须是以空字符结尾的,即以’’\0’结尾。未指定地址以空字符串的路径名表示,也就是一个sun_path[0]为’\0’。
例子:Unix域套接口的捆绑
下面的程序建立一个Unix域套接口,给它捆绑一个路径名,然后调用getsockname输出已绑定的路径名。
#include <sys/socket.h>
#include <sys/un.h>
int main(int argc, char* argv[]) {
int sockfd;
socklen_t len;
struct sockaddr_un addr1, addr2;
if(argc != 2) {
perror("usage: unixbind <pathname>");
exit(1);
}
sockfd = Socket(AF_LOCAL, SOCK_STREAM, 0);
unlink(argv[1]);
bzero(&addr1, sizeof(addr1));
addr1.sun_family = AF_LOCAL;
strncpy(addr1.sun_path, argv[1], sizeof(addr1.sun_path)-1);
Bind(sockfd, (SA*)&addr1, SUN_LEN(&addr1));
len = sizeof(addr2);
Getsockname(sockfd, (SA*)&addr2, &len);
printf("bound name = %s, returned len = %d\n", addr2.sun_path, len);
exit(0);
}
? unlink(argv[1])的作用是如果该路径名在系统中已存在,调用unlink将其删除。如果路径名不存在,unlink将会返回错误,程序将其忽略。
socketpair函数
? socketpair函数建立一对相互连接的套接口,这个函数只对Unix域套接口适用。
#include <sys/socket.h>
int socketpair(int family, int type, int protocol, int sockfd[2]);
? family必须为AF_LOCAL,protocol必须为0,type可以是SOCK_STREAM或SOCK_DGRAM。新创建的两个套接口描述字作为sockfd[0]和sockfd[1]返回。
? 创建的两个套接口是没有名字的,即没有涉及隐式bind。
? 以SOCKET_STREAM作为type调用socketpair所得到的结果称作流管道(stream pipe)。这和一般的Unix管道(由pipe函数生成)类似,但流管道是全双工的,即两个描述字都是可读写的。
套接口函数
? 当用于Unix套接口时,套接口函数有一些差别和限制:
- bind建立的路径名的缺省访问权限应为777,并被当前的umask修改。
- 与Unix域套接口相关联的路径名应为一个绝对路径名,而不是相对路径名。
- connect使用的路径名必须是一个绑定在某个已打开的Unix域套接口上的路径名,而且套接口的类型(字节流或数据包)也必须一致。下列情况将会出错:(a)该路径名存在但不是一个套接口;(b)路径名存在且是一个套接口,但没有与该路径名相关联的打开的描述字;?路径名存在且是一个打开的套接口,但类型不符——Unix域字节流套接口不能连到与Unix域数据包套接口相关联的路径名,反之亦然。
- 用connect连接Unix域套接口时权限检查和用open以只写方式访问路径名时完全相同。
- Unix域字节流套接口和TCP套接口类似:它们都为进程提供一个没有记录边界的字节流套接口。
- 如果Unix域字节流套接口的connect调用发现监听套接口的队列已满,会立刻返回一个ECONNECTREFUSED错误。这和TCP连接有所不同:如果监听套接口的队列已满,它将忽略到来的SYN,TCP连接的发起方会接着发送几次SYN重试。
- Unix域数据包套接口和UDP套接口类似:它们都提供一个保留记录边界的不可靠的数据包服务。
- 与UDP套接口不同的是,在未绑定的Unix域套接口上发送数据报不会给它捆绑一个路径名(回想在未绑定的UDP套接口上发送UDP数据包会为该套接口绑定一个临时端口)。这意味着,数据报的发送者除非绑定一个路径名,否者接收者无法发回应答数据报。同样,与TCP和UDP不同的是,给Unix域数据包套接口调用connect不会捆绑一个路径名。
例子Unix域字节流客户——服务器程序
本例同样来自[2]https://github.com/troydhanson/network
客户端
#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char *socket_path = "\0hidden";
int main(int argc, char *argv[]) {
struct sockaddr_un addr;
char buf[100];
int fd,rc;
if (argc > 1) socket_path=argv[1];
if ( (fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
perror("socket error");
exit(-1);
}
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
if (*socket_path == '\0') {
*addr.sun_path = '\0';
strncpy(addr.sun_path+1, socket_path+1, sizeof(addr.sun_path)-2);
} else {
strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path)-1);
}
if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("connect error");
exit(-1);
}
while( (rc=read(STDIN_FILENO, buf, sizeof(buf))) > 0) {
if (write(fd, buf, rc) != rc) {
if (rc > 0) fprintf(stderr,"partial write");
else {
perror("write error");
exit(-1);
}
}
}
return 0;
}
服务器
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <stdlib.h>
char *socket_path = "\0hidden";
int main(int argc, char *argv[]) {
struct sockaddr_un addr;
char buf[100];
int fd,cl,rc;
if (argc > 1) socket_path=argv[1];
if ( (fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
perror("socket error");
exit(-1);
}
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
if (*socket_path == '\0') {
*addr.sun_path = '\0';
strncpy(addr.sun_path+1, socket_path+1, sizeof(addr.sun_path)-2);
} else {
strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path)-1);
unlink(socket_path);
}
if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
perror("bind error");
exit(-1);
}
if (listen(fd, 5) == -1) {
perror("listen error");
exit(-1);
}
while (1) {
if ( (cl = accept(fd, NULL, NULL)) == -1) {
perror("accept error");
continue;
}
while ( (rc=read(cl,buf,sizeof(buf))) > 0) {
printf("read %u bytes: %.*s\n", rc, rc, buf);
}
if (rc == -1) {
perror("read");
exit(-1);
}
else if (rc == 0) {
printf("EOF\n");
close(cl);
}
}
return 0;
}
在Golang中使用Unix域套接字
? go的net包提供了对Unix域套接字的支持,涉及的结构体和方法如下
type UnixAddr
func ResolveUnixAddr(network, address string) (*UnixAddr, error)
func (a *UnixAddr) Network() string
func (a *UnixAddr) String() string
type UnixConn
func DialUnix(network string, laddr, raddr *UnixAddr) (*UnixConn, error)
func ListenUnixgram(network string, laddr *UnixAddr) (*UnixConn, error)
func (c *UnixConn) Close() error
func (c *UnixConn) CloseRead() error
func (c *UnixConn) CloseWrite() error
func (c *UnixConn) File() (f *os.File, err error)
func (c *UnixConn) LocalAddr() Addr
func (c *UnixConn) Read(b []byte) (int, error)
func (c *UnixConn) ReadFrom(b []byte) (int, Addr, error)
func (c *UnixConn) ReadFromUnix(b []byte) (int, *UnixAddr, error)
func (c *UnixConn) ReadMsgUnix(b, oob []byte) (n, oobn, flags int, addr *UnixAddr, err error)
func (c *UnixConn) RemoteAddr() Addr
func (c *UnixConn) SetDeadline(t time.Time) error
func (c *UnixConn) SetReadBuffer(bytes int) error
func (c *UnixConn) SetReadDeadline(t time.Time) error
func (c *UnixConn) SetWriteBuffer(bytes int) error
func (c *UnixConn) SetWriteDeadline(t time.Time) error
func (c *UnixConn) SyscallConn() (syscall.RawConn, error)
func (c *UnixConn) Write(b []byte) (int, error)
func (c *UnixConn) WriteMsgUnix(b, oob []byte, addr *UnixAddr) (n, oobn int, err error)
func (c *UnixConn) WriteTo(b []byte, addr Addr) (int, error)
func (c *UnixConn) WriteToUnix(b []byte, addr *UnixAddr) (int, error)
type UnixListener
func ListenUnix(network string, laddr *UnixAddr) (*UnixListener, error)
func (l *UnixListener) Accept() (Conn, error)
func (l *UnixListener) AcceptUnix() (*UnixConn, error)
func (l *UnixListener) Addr() Addr
func (l *UnixListener) Close() error
func (l *UnixListener) File() (f *os.File, err error)
func (l *UnixListener) SetDeadline(t time.Time) error
func (l *UnixListener) SetUnlinkOnClose(unlink bool)
func (l *UnixListener) SyscallConn() (syscall.RawConn, error)
UnixAddr
? UnixAddr用于存放unix域套接字。
type UnixAddr struct {
Name string
Net string
}
? Net可以是"unix", “unixgram"和"unixpacket”。unix类似于TCP,unixgram类似于udp,unixpacket类似于可以保证数据包顺序的udp[5]。
ResovleUnixAddr
ResolveUnixAddr根据给出的网络类型和地址,创建一个UnixAddr对象。
func ResolveUnixAddr(network, address string) (*UnixAddr, error)
UnixConn
UnixConn是Conn接口的Unix域套接字实现。
type UnixConn struct {
}
DialUnix
DialUnix连接到raddr的Unix域套接字。
func DialUnix(network string, laddr, raddr *UnixAddr) (*UnixConn, error)
Close
关闭Unix域套接字连接
func (c *UnixConn) Close() error
UnixListener
UnixListener是一个unix域套接字的监听者。
type UnixListener struct {
}
ListenUnix
类似于unix套接字的listen。其中network必须是"unix"或者"unixpacket"。
func ListenUnix(network string, laddr *UnixAddr) (*UnixListener, error)
Accept
accept是Listen接口的unix域套接字实现。返回的conn是*UnixConn类型的。
func (l *UnixListener) Accept() (Conn, error)
Close
Close停止在Unix域地址上的监听。已经建立的连接不会被关闭。
func (l *UnixListener) Close() error
? 在go中使用unix域套接口编程涉及的结构体就是UnixConn和UnixListener,其中客户端只需要使用UnixConn,服务器则需要使用UnixListener接受连接,并利用Accept方法返回的UnixConn处理和某个客户端的连接,前者是监听套接字,后者是已连接套接字。
Golang的Unix域字节流客户——服务器程序
本例子来自[7]https://gist.github.com/hakobe/6f70d69b8c5243117787fd488ae7fbf2
客户端client.go
package main
import (
"io"
"log"
"net"
"time"
)
func reader(r io.Reader) {
buf := make([]byte, 1024)
for {
n, err := r.Read(buf[:])
if err != nil {
return
}
println("Client got:", string(buf[0:n]))
}
}
func main() {
c, err := net.Dial("unix", "/tmp/go.sock")
if err != nil {
log.Fatal("Dial error", err)
}
defer c.Close()
go reader(c)
for {
msg := "hi"
_, err := c.Write([]byte(msg))
if err != nil {
log.Fatal("Write error:", err)
break
}
println("Client sent:", msg)
time.Sleep(1 * time.Second)
}
}
服务器server.go
package main
import (
"log"
"net"
"os"
"os/signal"
"syscall"
)
func echoServer(c net.Conn) {
for {
buf := make([]byte, 512)
nr, err := c.Read(buf)
if err != nil {
return
}
data := buf[0:nr]
println("Server got:", string(data))
_, err = c.Write(data)
if err != nil {
log.Fatal("Writing client error: ", err)
}
}
}
func main() {
log.Println("Starting echo server")
ln, err := net.Listen("unix", "/tmp/go.sock")
if err != nil {
log.Fatal("Listen error: ", err)
}
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, syscall.SIGTERM)
go func(ln net.Listener, c chan os.Signal) {
sig := <-c
log.Printf("Caught signal %s: shutting down.", sig)
ln.Close()
os.Exit(0)
}(ln, sigc)
for {
fd, err := ln.Accept()
if err != nil {
log.Fatal("Accept error: ", err)
}
go echoServer(fd)
}
}
总结
? 所有的客户和服务器都从调用socket开始,返回一个套接口描述字。然后客户调用connect,服务器调用bind、listen和accept。套接口一般由标准的close函数关闭,当然也可以用shutdown函数关闭。
? 客户和服务器在同一台主机上时,Unix域套接口是IPC的一种替代方法。和IPC相比使用Unix域套接字的好处在于,对于网络客户——服务器的API几乎是相同的。当客户和服务器在同一台主机上时,Unix域套接字和TCP相比,其性能比在多数实现中要更高。
参考资料:
[1].Unix网络编程第一卷.W.Richard Stevens著. 清华大学出版社
[2].https://github.com/troydhanson/network
[3].https://linux.die.net/man/2/
[4].The Linux socket API explained.https://www.youtube.com/watch?v=XXfdzwEsxFk
[5].https://stackoverflow.com/questions/22955226/unix-domain-socket-name-in-go-language
[6].go net package. https://pkg.go.dev/net@go1.16.6
[7].https://gist.github.com/hakobe/6f70d69b8c5243117787fd488ae7fbf2
|