从今天开始,正式进入MiniFtp的代码编写阶段了,好兴奋,接下来很长一段时间会将整个实现过程从无到有一点点实现出来,达到综合应用的效果,话不多说正入正题:
这节主要是将基础代码框架搭建好,基于上节介绍的系统逻辑结构,首先建立主控模块:
在学习网络编程时积累了不少的工具代码,所以可以将其整合到系统工具模块:
sysutil.h:
#ifndef _SYS_UTIL_H_ #define _SYS_UTIL_H_int getlocalip(char *ip); void activate_nonblock(int fd); void deactivate_nonblock(int fd); int read_timeout(int fd, unsigned int wait_seconds); int write_timeout(int fd, unsigned int wait_seconds); int accept_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds); int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds); ssize_t readn(int fd, void *buf, size_t count); ssize_t writen(int fd, const void *buf, size_t count); ssize_t recv_peek(int sockfd, void *buf, size_t len); ssize_t readline(int sockfd, void *buf, size_t maxline); void send_fd(int sock_fd, int fd); int recv_fd(const int sock_fd); #endif /* _SYS_UTIL_H_ */
sysutil.c:
#include "sysutil.h"int getlocalip(char *ip) { char host[100] = {0}; if (gethostname(host, sizeof(host)) < 0) return -1; struct hostent *hp; if ((hp = gethostbyname(host)) == NULL) return -1; strcpy(ip, inet_ntoa(*(struct in_addr*)hp->h_addr)); return 0; } /** * activate_noblock - 设置I/O为非阻塞模式 * @fd: 文件描符符 */ void activate_nonblock(int fd) { int ret; int flags = fcntl(fd, F_GETFL); if (flags == -1) ERR_EXIT("fcntl"); flags |= O_NONBLOCK; ret = fcntl(fd, F_SETFL, flags); if (ret == -1) ERR_EXIT("fcntl"); } /** * deactivate_nonblock - 设置I/O为阻塞模式 * @fd: 文件描符符 */ void deactivate_nonblock(int fd) { int ret; int flags = fcntl(fd, F_GETFL); if (flags == -1) ERR_EXIT("fcntl"); flags &= ~O_NONBLOCK; ret = fcntl(fd, F_SETFL, flags); if (ret == -1) ERR_EXIT("fcntl"); } /** * read_timeout - 读超时检测函数,不含读操作 * @fd: 文件描述符 * @wait_seconds: 等待超时秒数,如果为0表示不检测超时 * 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT */ int read_timeout(int fd, unsigned int wait_seconds) { int ret = 0; if (wait_seconds > 0) { fd_set read_fdset; struct timeval timeout; FD_ZERO(&read_fdset); FD_SET(fd, &read_fdset); timeout.tv_sec = wait_seconds; timeout.tv_usec = 0; do { ret = select(fd + 1, &read_fdset, NULL, NULL, &timeout); } while (ret < 0 && errno == EINTR); if (ret == 0) { ret = -1; errno = ETIMEDOUT; } else if (ret == 1) ret = 0; } return ret; } /** * write_timeout - 读超时检测函数,不含写操作 * @fd: 文件描述符 * @wait_seconds: 等待超时秒数,如果为0表示不检测超时 * 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT */ int write_timeout(int fd, unsigned int wait_seconds) { int ret = 0; if (wait_seconds > 0) { fd_set write_fdset; struct timeval timeout; FD_ZERO(&write_fdset); FD_SET(fd, &write_fdset); timeout.tv_sec = wait_seconds; timeout.tv_usec = 0; do { ret = select(fd + 1, NULL, NULL, &write_fdset, &timeout); } while (ret < 0 && errno == EINTR); if (ret == 0) { ret = -1; errno = ETIMEDOUT; } else if (ret == 1) ret = 0; } return ret; } /** * accept_timeout - 带超时的accept * @fd: 套接字 * @addr: 输出参数,返回对方地址 * @wait_seconds: 等待超时秒数,如果为0表示正常模式 * 成功(未超时)返回已连接套接字,超时返回-1并且errno = ETIMEDOUT */ int accept_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds) { int ret; socklen_t addrlen = sizeof(struct sockaddr_in); if (wait_seconds > 0) { fd_set accept_fdset; struct timeval timeout; FD_ZERO(&accept_fdset); FD_SET(fd, &accept_fdset); timeout.tv_sec = wait_seconds; timeout.tv_usec = 0; do { ret = select(fd + 1, &accept_fdset, NULL, NULL, &timeout); } while (ret < 0 && errno == EINTR); if (ret == -1) return -1; else if (ret == 0) { errno = ETIMEDOUT; return -1; } } if (addr != NULL) ret = accept(fd, (struct sockaddr*)addr, &addrlen); else ret = accept(fd, NULL, NULL); /* if (ret == -1) ERR_EXIT("accept"); */ return ret; } /** * connect_timeout - connect * @fd: 套接字 * @addr: 要连接的对方地址 * @wait_seconds: 等待超时秒数,如果为0表示正常模式 * 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT */ int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds) { int ret; socklen_t addrlen = sizeof(struct sockaddr_in); if (wait_seconds > 0) activate_nonblock(fd); ret = connect(fd, (struct sockaddr*)addr, addrlen); if (ret < 0 && errno == EINPROGRESS) { printf("AAAAA "); fd_set connect_fdset; struct timeval timeout; FD_ZERO(&connect_fdset); FD_SET(fd, &connect_fdset); timeout.tv_sec = wait_seconds; timeout.tv_usec = 0; do { /* 一量连接建立,套接字就可写 */ ret = select(fd + 1, NULL, &connect_fdset, NULL, &timeout); } while (ret < 0 && errno == EINTR); if (ret == 0) { ret = -1; errno = ETIMEDOUT; } else if (ret < 0) return -1; else if (ret == 1) { printf("BBBBB "); /* ret返回为1,可能有两种情况,一种是连接建立成功,一种是套接字产生错误,*/ /* 此时错误信息不会保存至errno变量中,因此,需要调用getsockopt来获取。 */ int err; socklen_t socklen = sizeof(err); int sockoptret = getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &socklen); if (sockoptret == -1) { return -1; } if (err == 0) { printf("DDDDDDD "); ret = 0; } else { printf("CCCCCC "); errno = err; ret = -1; } } } if (wait_seconds > 0) { deactivate_nonblock(fd); } return ret; } /** * readn - 读取固定字节数 * @fd: 文件描述符 * @buf: 接收缓冲区 * @count: 要读取的字节数 * 成功返回count,失败返回-1,读到EOF返回<count */ ssize_t readn(int fd, void *buf, size_t count) { size_t nleft = count; ssize_t nread; char *bufp = (char*)buf; while (nleft > 0) { if ((nread = read(fd, bufp, nleft)) < 0) { if (errno == EINTR) continue; return -1; } else if (nread == 0) return count - nleft; bufp += nread; nleft -= nread; } return count; } /** * writen - 发送固定字节数 * @fd: 文件描述符 * @buf: 发送缓冲区 * @count: 要读取的字节数 * 成功返回count,失败返回-1 */ ssize_t writen(int fd, const void *buf, size_t count) { size_t nleft = count; ssize_t nwritten; char *bufp = (char*)buf; while (nleft > 0) { if ((nwritten = write(fd, bufp, nleft)) < 0) { if (errno == EINTR) continue; return -1; } else if (nwritten == 0) continue; bufp += nwritten; nleft -= nwritten; } return count; } /** * recv_peek - 仅仅查看套接字缓冲区数据,但不移除数据 * @sockfd: 套接字 * @buf: 接收缓冲区 * @len: 长度 * 成功返回>=0,失败返回-1 */ ssize_t recv_peek(int sockfd, void *buf, size_t len) { while (1) { int ret = recv(sockfd, buf, len, MSG_PEEK); if (ret == -1 && errno == EINTR) continue; return ret; } } /** * readline - 按行读取数据 * @sockfd: 套接字 * @buf: 接收缓冲区 * @maxline: 每行最大长度 * 成功返回>=0,失败返回-1 */ ssize_t readline(int sockfd, void *buf, size_t maxline) { int ret; int nread; char *bufp = buf; int nleft = maxline; while (1) { ret = recv_peek(sockfd, bufp, nleft); if (ret < 0) return ret; else if (ret == 0) return ret; nread = ret; int i; for (i=0; i<nread; i++) { if (bufp[i] == ' ') { ret = readn(sockfd, bufp, i+1); if (ret != i+1) exit(EXIT_FAILURE); return ret; } } if (nread > nleft) exit(EXIT_FAILURE); nleft -= nread; ret = readn(sockfd, bufp, nread); if (ret != nread) exit(EXIT_FAILURE); bufp += nread; } return -1; } void send_fd(int sock_fd, int fd) { int ret; struct msghdr msg; struct cmsghdr *p_cmsg; struct iovec vec; char cmsgbuf[CMSG_SPACE(sizeof(fd))]; int *p_fds; char sendchar = 0; msg.msg_control = cmsgbuf; msg.msg_controllen = sizeof(cmsgbuf); p_cmsg = CMSG_FIRSTHDR(&msg); p_cmsg->cmsg_level = SOL_SOCKET; p_cmsg->cmsg_type = SCM_RIGHTS; p_cmsg->cmsg_len = CMSG_LEN(sizeof(fd)); p_fds = (int*)CMSG_DATA(p_cmsg); *p_fds = fd; msg.msg_name = NULL; msg.msg_namelen = 0; msg.msg_iov = &vec; msg.msg_iovlen = 1; msg.msg_flags = 0; vec.iov_base = &sendchar; vec.iov_len = sizeof(sendchar); ret = sendmsg(sock_fd, &msg, 0); if (ret != 1) ERR_EXIT("sendmsg"); } int recv_fd(const int sock_fd) { int ret; struct msghdr msg; char recvchar; struct iovec vec; int recv_fd; char cmsgbuf[CMSG_SPACE(sizeof(recv_fd))]; struct cmsghdr *p_cmsg; int *p_fd; vec.iov_base = &recvchar; vec.iov_len = sizeof(recvchar); msg.msg_name = NULL; msg.msg_namelen = 0; msg.msg_iov = &vec; msg.msg_iovlen = 1; msg.msg_control = cmsgbuf; msg.msg_controllen = sizeof(cmsgbuf); msg.msg_flags = 0; p_fd = (int*)CMSG_DATA(CMSG_FIRSTHDR(&msg)); *p_fd = -1; ret = recvmsg(sock_fd, &msg, 0); if (ret != 1) ERR_EXIT("recvmsg"); p_cmsg = CMSG_FIRSTHDR(&msg); if (p_cmsg == NULL) ERR_EXIT("no passed fd"); p_fd = (int*)CMSG_DATA(p_cmsg); recv_fd = *p_fd; if (recv_fd == -1) ERR_EXIT("no passed fd"); return recv_fd; }
以上两个的具体实现之前都已经学过了,这里就不一一描述了。
对于这些函数会用到一些头文件,这里统一放到一个头文件中,集中管理:
common.h:
#ifndef _COMMON_H_ #define _COMMON_H_ #include <unistd.h> #include <sys/types.h> #include <fcntl.h> #include <errno.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); } while (0) #endif /* _COMMON_H_ */
下面来编译运行一下,确保目前代码的正确性,所以还需要准备一个Makefile文件:
Makefile:
.PHONY:clean CC=gcc CFLAGS=-Wall -g BIN=miniftpd OBJS=main.o sysutil.o $(BIN):$(OBJS) $(CC) $(CFLAGS) $^ -o $@ %.o:%.c $(CC) $(CFLAGS) -c $< -o $@ clean: rm -f *.o $(BIN)
再次编译:
目前的代码都正常了,接下来正式一步步编写有效代码,由于ftp是需要root权限的,所以第一步先来做root权限的判断:
编译运行:
接下来,MiniFtp是一个服务器端,它都有一个这样的步骤:创建套接字、绑定监听、接受连接、处理连接,所以可以将其封装到一个函数当中:
接下来具体实现它:
接下来绑定监听,首先准备sockaddr_in参数:
接下来则可以开始绑定地址了:
int tcp_server(const char *host, unsigned short port) { //创建套接字 int listenfd; if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0) ERR_EXIT("tcp_server"); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; if (host != NULL) { if (inet_aton(host, &servaddr.sin_addr) == 0) {//证明传过来的是主机名而不是点分十进制的IP地址,接下来要进行转换 struct hostent *hp; hp = gethostbyname(host); if (hp == NULL) ERR_EXIT("gethostbyname"); servaddr.sin_addr = *(struct in_addr*)hp->h_addr; } } else//这时用主机的任务地址 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(port);//端口号 //设置地址重复利用 int on = 1; if ((setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const char*)&on, sizeof(on))) < 0) ERR_EXIT("gethostbyname"); //绑定 if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) ERR_EXIT("bind"); return listenfd; }
最后监听:
/** * tcp_server - 启动tcp服务器 * @host: 服务器IP地址或者服务器主机名 * @port: 服务器端口 * 成功返回监听套接字 */ int tcp_server(const char *host, unsigned short port) { //创建套接字 int listenfd; if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0) ERR_EXIT("tcp_server"); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; if (host != NULL) { if (inet_aton(host, &servaddr.sin_addr) == 0) {//证明传过来的是主机名而不是点分十进制的IP地址,接下来要进行转换 struct hostent *hp; hp = gethostbyname(host); if (hp == NULL) ERR_EXIT("gethostbyname"); servaddr.sin_addr = *(struct in_addr*)hp->h_addr; } } else//这时用主机的任务地址 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(port);//端口号 //设置地址重复利用 int on = 1; if ((setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const char*)&on, sizeof(on))) < 0) ERR_EXIT("gethostbyname"); //绑定 if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) ERR_EXIT("bind"); //监听 if (listen(listenfd, SOMAXCONN) < 0) ERR_EXIT("listen"); return listenfd; }
接下来编译一下,程序一步步稳步开发:
编写好这个函数之后,则在main函数中去调用一下:
接着则要编写接受客户端的连接:
下面来新建session模块所需的文件:
session.h:
#ifndef _SESSION_H_ #define _SESSION_H_ #include "common.h" void begin_session(int conn); #endif /* _SESSION_H_ */
session.c:
#include "common.h" #include "session.h" void begin_session(int conn) { }
然后在main.c中包含它:
接下来来实现begin_session这个方法,而根据上次介绍的逻辑结构来看:
所以需要创建两个进程:
然后再把这两个进程做的事也模块化,FTP服务进程主要是处理FTP协议相关的一些细节,模块可以叫ftpproto,而nobody进程主要是协助FTP服务进程,只对内,模块可以叫privparent,所以可以新建如下文件:
所以这里需要建立一个通道来让两进程之间可以相互通信,这里采用socketpair来进行通信:
另外可以定义一个session结构体来代表一个会话,里面包含多个信息:
session.h:
#ifndef _SESSION_H_ #define _SESSION_H_ #include "common.h" typedef struct session { // 控制连接 int ctrl_fd; char cmdline[MAX_COMMAND_LINE]; char cmd[MAX_COMMAND]; char arg[MAX_ARG]; // 父子进程通道 int parent_fd; int child_fd; } session_t; void begin_session(session_t *sess); #endif /* _SESSION_H_ */
上面用到了三个宏,也需要在common.h中进行定义:
这时在main中就得声明一下该session,并将其传递:
这时再回到begin_session方法中,进一步带到父子进程中去处理:
下面则在session的父子进程中进行函数的声明:
ftpproto.h:
#ifndef _FTP_PROTO_H_ #define _FTP_PROTO_H_ #include "session.h" void handle_child(session_t *sess); #endif /* _FTP_PROTO_H_ */
ftpproto.c:
#include "ftpproto.h" #include "sysutil.h" void handle_child(session_t *sess) { }
privparent.h:
#ifndef _PRIV_PARENT_H_ #define _PRIV_PARENT_H_ #include "session.h" void handle_parent(session_t *sess); #endif /* _PRIV_PARENT_H_ */
privparent.c:
#include "privparent.h" void handle_parent(session_t *sess) { }
在session.c中需要包含这两个头文件:
接下来我们将注意力集中在begin_session函数中,首先我们需要将父进程改成nobody进程,怎么来改呢?这里需要用到一个函数:
下面来编写handle_child()和handle_parent():
另外在连接时,会给客户端一句这样的提示语:
所以:
这个函数暂且这样,接着来编写handle_parent():
这次主要是搭建基本框架,所以里面的基本都是虚实现,下面来编译运行看下效果:
先修改Makefile文件:
查看一下man帮助:
所以在common.h中添加该头文件:
再次编译:
类型参数不对,查看一下,目前还是int类型,应该改为session_t:
修改为:
再次编译:
接下来运行一下:
这时查看下当前进程状态:
接下来开一个FTP客户端来进行连接:
这时再查看进程状态:
而vsftpd的进程模型为:
这时由于还没有处理USER webor2006命令:
处理之后就和vsftpd一样了,以上就是miniftp的一个基本框架,下次继续。