域套接字的监听、接受、连接(listen、accept、connect)
- 域套接字可以调用网络套接字函数接口来实现不同进程之间的通信。类似于与TCP、UDP之间的通信
- 下面是我们自定义的3个函数:
一、serv_listen函数
- 参数name:服务端调用serv_listen函数声明它要在一个众所周知的名字上监听客户进程的连接请求。客户端通过这个名字来连接到服务器
- 返回值:成功服务端要监听的域套接字描述符,出错返回负值
- 此函数中包含了服务端的socket、bind、listen三个步骤的函数
- 调用unlink()函数的原因:调用bind绑定域套接字地址时,如果文件名已存在,则bind会失败,因此要调用先调用unlink删除这个路径名,以防止该文件已经存在
#define QLEN 10
int serv_listen(const char *name)
{
int fd, len, err, rval;
struct sockaddr_un un;
if (strlen(name) >= sizeof(un.sun_path)) {
errno = ENAMETOOLONG;
return(-1);
}
//创建域套接字
if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
return(-2);
unlink(name);
//初始化域套接字地址信息
memset(&un, 0, sizeof(un));
un.sun_family = AF_UNIX;
strcpy(un.sun_path, name);
len = offsetof(struct sockaddr_un, sun_path) + strlen(name);
//为域套接字绑定地址
if (bind(fd, (struct sockaddr *)&un, len) < 0) {
rval = -3;
goto errout;
}
//监听客户进程连接请求
if (listen(fd, QLEN) < 0) {
rval = -4;
goto errout;
}
return(fd);
errout:
err = errno;
close(fd);
errno = err;
return(rval);
}
二、serv_accept函数
- 此函数用来等待接受客户端的请求,模仿accept函数,因为此函数也是阻塞函数。直到一个客户端调用connect函数连接才返回
- listenfd参数:服务端的域套接字
- uidptr参数:当有一个客户进程连接之后,此参数存放客户进程的有效用户ID
- 返回值:成功返回连接到服务端的客户端的域套接字描述符。出错返回-1
- 下面的代码有很多的验证。虽然下面这些检验并不完善,但是对当前系统所能做到的最佳方案
#define STALE 30 /* client's name can't be older than this (sec) */
int serv_accept(int listenfd, uid_t *uidptr)
{
int clifd, err, rval;
socklen_t len;
time_t staletime;
struct sockaddr_un un;
struct stat statbuf;
char *name;
if ((name = malloc(sizeof(un.sun_path + 1))) == NULL)
return(-1);
len = sizeof(un);
if ((clifd = accept(listenfd, (struct sockaddr *)&un, &len)) < 0) {
free(name);
return(-2); /* often errno=EINTR, if signal caught */
}
len -= offsetof(struct sockaddr_un, sun_path); /* len of pathname */
//复制客户进程赋给其套接字的路径名
memcpy(name, un.sun_path, len);
name[len] = 0; //赋值0,确保路径名为null终止
//调用stat函数验证:该路径名确实是一个套接字
if (stat(name, &statbuf) < 0) {
rval = -3;
goto errout;
}
#ifdef S_ISSOCK /* not defined for SVR4 */
if (S_ISSOCK(statbuf.st_mode) == 0) {
rval = -4; /* not a socket */
goto errout;
}
#endif
//验证其权限仅允许用户读、写以及用户执行
if ((statbuf.st_mode & (S_IRWXG | S_IRWXO)) ||
(statbuf.st_mode & S_IRWXU) != S_IRWXU) {
rval = -5; /* is not rwx------ */
goto errout;
}
//验证与套接字相关的3个时间参数不必当前时间早30秒
staletime = time(NULL) - STALE;
if (statbuf.st_atime < staletime ||
statbuf.st_ctime < staletime ||
statbuf.st_mtime < staletime) {
rval = -6; /* i-node is too old */
goto errout;
}
if (uidptr != NULL)
*uidptr = statbuf.st_uid; /* return uid of caller */
unlink(name);
free(name);
return(clifd);
errout:
err = errno;
close(clifd);
free(name);
errno = err;
return(rval);
}
三、cli_conn函数
- 客户端调用函数创建客户端域套接字并绑定地址,最后再connect连接服务端
- name参数:与要连接的客户端的域套接字绑定的sockaddr_un中的sun_path名字相同
- 返回值:成功返回服务端的域套接字描述符。出错返回负数
- 客户端绑定地址时,要显式地为客户端绑定自己的地址。如果不绑定,那么系统不会自动给这个域套接字绑定一个路径名,所以服务端接收到这个没有绑定地址的消息时,无法发回应数据报。这一点与UDP/TCP不同,如果UDP/TCP客户端套接字没有绑定地址,系统会自动分配
- 如果connect连接服务端时如果发现监听域套接字队列已满,则返回一个ECONNREFUSED错误。这一点不同于TCP,TCP如果监听套接字的队列已满,TCP监听就忽略新到达的SYN,而TCP连接发起端将数次发送SYN重试
#define CLI_PATH "/var/tmp/"
#define CLI_PERM S_IRWXU /* rwx for user only */
int cli_conn(const char *name)
{
int fd, len, err, rval;
struct sockaddr_un un, sun;
int do_unlink = 0;
if (strlen(name) >= sizeof(un.sun_path)) {
errno = ENAMETOOLONG;
return(-1);
}
if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
return(-1);
memset(&un, 0, sizeof(un));
un.sun_family = AF_UNIX;
sprintf(un.sun_path, "%s%05ld", CLI_PATH, (long)getpid());
printf("file is %s\n", un.sun_path);
len = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
unlink(un.sun_path);
if (bind(fd, (struct sockaddr *)&un, len) < 0) {
rval = -2;
goto errout;
}
//调用chmod关闭除用户读、写以及用户执行以外的其它权限
if (chmod(un.sun_path, CLI_PERM) < 0) {
rval = -3;
do_unlink = 1;
goto errout;
}
memset(&sun, 0, sizeof(sun));
sun.sun_family = AF_UNIX;
strcpy(sun.sun_path, name);
len = offsetof(struct sockaddr_un, sun_path) + strlen(name);
if (connect(fd, (struct sockaddr *)&sun, len) < 0) {
rval = -4;
do_unlink = 1;
goto errout;
}
return(fd);
errout:
err = errno;
close(fd);
if (do_unlink)
unlink(un.sun_path);
errno = err;
return(rval);
}