项目简介:
在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务进程来为客户提供服务。同时每个ftp服务进程配套了nobody进程(内部私有进程),主要是为了做权限提升和控制。
实现功能:
除了基本的文件上传和下载功能,还实现模式选择、断点续传、限制连接数、空闲断开、限速等功能。
用到的技术:
socket、I/O复用、进程间通信、HashTable
欢迎技术交流q:2723808286
项目开源!!!

miniFTP项目实战一
miniFTP项目实战二
miniFTP项目实战三
miniFTP项目实战四
miniFTP项目实战五
miniFTP项目实战六

传输目录列表

在用户登录之后,客户端会与服务器协商,传输的文件类型以及传输的类型,之后再LIST申请目录列表:

文件的传输类型一般都是ASCII,传输模式需要根据有无NAT防火墙来选择,具体的确保在之前以及介绍过了。

4.1 PASV模式

服务器被动连接,由nobody进程创建监听socket,将创建好的监听socket传递给服务进程,服务进程返回服务器的IP地址及端口号,其处理逻辑如下:

static void do_pasv(session_t *sess)
{
//由nobody进程创建监听套接字 ,并返回端口 服务进程中通过getlocalip获取IP地址
priv_sock_send_cmd(sess->child_fd, PRIV_SOCK_PASV_LISTEN);
unsigned short port = (int)priv_sock_get_int(sess->child_fd); //获取端口号 char ip[16] = {0};
int ret = getlocalip(ip);
if (ret == -1) printf("getlocalip filed\n"); char tmp[1024] = {0};
unsigned int v[4];
sscanf(ip, "%u.%u.%u.%u", &v[0], &v[1], &v[2], &v[3]);
sprintf(tmp, "Entering Passive Mode (%u,%u,%u,%u,%u,%u)",
v[0], v[1], v[2], v[3], port>>8, port&0xff); ftp_relply(sess, FTP_PASVOK, tmp);
}

由于getlocalip(ip)获取的是点分形式的IP地址,所以通过sscanf格式化获取IP地址,然后格式化到tmp字符串中返回给客户端。

4.2 PORT模式

PORT模式下,服务器主动连接客户端,客户端的命令参数中说明了客户端的IP地址以及端口号,服务进程解析IP地址及端口号,保存在sess中,后续nobody进程根据sess中的IP地址进行连接,处理逻辑如下:

static void do_port(session_t *sess)
{
unsigned int v[6] = {0}; //直接使用sscanf格式化输入,提取相关数据
sscanf(sess->arg, "%u,%u,%u,%u,%u,%u", &v[2], &v[3], &v[4], &v[5], &v[0], &v[1]);
sess->port_addr = (struct sockaddr_in*)malloc(sizeof(struct sockaddr_in));
memset(sess->port_addr, 0, sizeof(struct sockaddr_in)); sess->port_addr->sin_family = AF_INET;
unsigned char *p = (unsigned char*)&sess->port_addr->sin_port;
p[0] = v[0];
p[1] = v[1];
p = (unsigned char*)&sess->port_addr->sin_addr;
p[0] = v[2];
p[1] = v[3];
p[2] = v[4];
p[3] = v[5]; //收到PORT后要回复
ftp_relply(sess, FTP_PORTOK, "PORT command sucessful. COnsider using PASV.");
//接下来客户端会发送LIST命令
}

4.3 LIST 命令中创建数据连接

PASV和PORT已经确定了双方的连接方式,LIST命令是传输当前目录的文件列表,首先应该创建数据传输通道!

数据传输通道创建完成之后,读取目录列表信息,经过提取信息后传输目录列表,最后关闭数据传输通道,回应226

在数据传输完成之后要及时关闭数据传输通道,即socket,因为客户端处于一直接收的状态,只有服务器关闭socket客户端才会停止接收,作出反应,逻辑如下:

static void do_list(session_t *sess)
{
//获取数据传输通道的fd
if (get_transfer_fd(sess) == 0) {
return ;
}
//回应150
ftp_relply(sess, FTP_DATACONN, "Here comes the directory listing.");
//传输列表
list_common(sess, 1); //全部
//关闭数据通道 如果不及时关闭通道 客户端是不会接收停止的,即关闭之后客户端才会作出反应
close(sess->data_fd);
//回应226
ftp_relply(sess, FTP_TRANSFEROK, "Directory send ok.");
}

其中最重要的有两部分:

  • 数据传输通道的创建,即nobody通过socket与客户端建立连接
  • 传输目录列表,要读取当前目录的信息,并且获取文件的信息发送

4.4 数据传输通道的建立

会根据PORT还是PASV来创建数据传输通道,所以在连接之前首先判断是PORT模式还是PASV模式,处理逻辑如下:

/*
* 根据模式的不同建立数据连接通道
* PORT:主动连接客户端
* PASV:被动接受客户端连接
* */
int get_transfer_fd(session_t *sess)
{
int ret = 1;
//检测 PORT or PASV 是否都没有激活
if (!port_active(sess) && !pasv_active(sess)) {
ftp_relply(sess, FTP_BADSENDCONN, "Use PORT or PASV first.");
return 0;
} //主动模式服务器绑定20端口 创建socket主动connect客户端,调用sysutil.c中实现的tcp_client
if (port_active(sess)) {
if (get_port_fd(sess) == 0) { //失败
ret = 0;
}
}
if (pasv_active(sess)) {
if (get_pasv_fd(sess) == 0) { //获取到之后就保存在sess->data_fd
ret = 0;
}
//监听socket作用就是 为数据连接通道做准备,每次数据连接完成之后都会断开,下一次重新连接
close(sess->pasv_listen_fd);
}
//malloc的地址已经没有利用价值了,以及绑定好啦
if (sess->port_addr != NULL) {
free(sess->port_addr);
sess->port_addr = NULL;
}
if (ret) start_data_alarm(); //在数据传输之前重新安装信号 并启动闹钟 return ret;
}

4.5 PORT模式下数据传输通道的创建

//获取PORT模式下数据传输通道的fd
int get_port_fd(session_t *sess)
{
//由nobody进程创建数据连接通道,服务进程向nobody发起一个PRIV_SOCK_GET_DATA_SOCK创建数据通道请求
priv_sock_send_cmd(sess->child_fd, PRIV_SOCK_GET_DATA_SOCK);
//然后向nobody发送一个int port
unsigned short port = ntohs(sess->port_addr->sin_port);
priv_sock_send_int(sess->child_fd, (int)port); //发送实际是short 强转为int
//然后向nobody发送IP地址 字符串
char *ip = inet_ntoa(sess->port_addr->sin_addr);
priv_sock_send_buf(sess->child_fd, ip, strlen(ip)); //接收应答判断
int res = priv_sock_get_result(sess->child_fd);
if (res == PRIV_SOCK_RESULT_BAD) {
printf("create data filed\n");
return 0;
} else if (res == PRIV_SOCK_RESULT_OK) {
sess->data_fd = priv_sock_recv_fd(sess->child_fd); //接收数据传输通道sock fd
} return 1;
}

服务进程向nobody进程发送PRIV_SOCK_GET_DATA_SOCK,请求nobody进程建立数据传输通道,接着向nobody进程发送客户端的IP地址与端口号:

void privop_pasv_get_data_sock(session_t *sess)
{
//接收IP地址与端口
unsigned short port = priv_sock_get_int(sess->parent_fd);
char ip[16] = {0};
priv_sock_recv_buf(sess->parent_fd, ip, sizeof(ip)); struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port); //转换为网络字节序
addr.sin_addr.s_addr = inet_addr(ip); int fd = tcp_client(20); //绑定20端口
if (fd == -1) {
priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_BAD);
return ;
} //建立数据连接
if (connect_timeout(fd, &addr, tunable_connect_timeout) < 0) {
priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_BAD);
return ;
} //传递文件描述符
priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_OK);
priv_sock_send_fd(sess->parent_fd, fd);
close(fd);
}

4.6PASV模式下数据传输通道的创建

int get_pasv_fd(session_t *sess)
{
//请求PASV 连接socket fd
priv_sock_send_cmd(sess->child_fd, PRIV_SOCK_PASV_ACCEPT);
char ret = priv_sock_get_result(sess->child_fd);
if (ret == PRIV_SOCK_RESULT_BAD) {
return 0;
} else if (ret == PRIV_SOCK_RESULT_OK) {
sess->data_fd = priv_sock_recv_fd(sess->child_fd);
} return 1;
}

PASV模式下,服务进程向nobody进程发送PRIV_SOCK_PASV_ACCEPT,nobody通过accept_timeout等待客户端的连接请求,当连接建立之后,nobody向服务进程传递数据传输通道的文件描述符,nobody进程的响应如下:

//服务进程请求数据连接socket的时候会向nobody发送accept请求
void privop_pasv_accept(session_t *sess)
{
int data_fd = accept_timeout(sess->pasv_listen_fd, NULL, tunable_accept_timeout);
close(sess->pasv_listen_fd);
sess->pasv_listen_fd = -1;
if (data_fd == -1) {
priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_BAD);
return ;
} else {
priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_OK);
priv_sock_send_fd(sess->parent_fd, data_fd);
close(data_fd); //nobody进程不进行数据传输 断开
}
}

4.7目录的传输

无论PORT模式的数据传输,还是PASV模式的数据传输,对服务进程都一样!!!因为服务进程最终拿到的是数据传输通道的socket文件描述符,服务进程可以通过这个socket文件描述符向客户端发送数据。

在处理LIST命令的时候逻辑如下:

static void do_list(session_t *sess)
{
//获取数据传输通道的fd
if (get_transfer_fd(sess) == 0) {
return ;
}
//回应150
ftp_relply(sess, FTP_DATACONN, "Here comes the directory listing.");
//传输列表
list_common(sess, 1); //全部
//关闭数据通道 如果不及时关闭通道 客户端是不会接收停止的,即关闭之后客户端才会作出反应
close(sess->data_fd);
//回应226
ftp_relply(sess, FTP_TRANSFEROK, "Directory send ok.");
}

上面已经介绍了get_transfer_fd是如何创建数据传输通道的,下面就说一下list_common如何传输列表信息。

需要传输的列表信息有:

  • 文件类型以及权限
  • 文件连接数、uid、gid、大小
  • 文件日期,分为两种格式
  • 文件名,注意符号链接文件还要显式支持原文件名字

如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VkMvU1PV-1613482997348)(F:\destop\m笔记\图\image-20210216181605784.png)]

打开当前目录

首先要打开当前目录,通过opendir函数可以打开目录,其函数原型如下:

DIR *opendir(const char *name);
//The opendir() function opens a directory stream corresponding to the directory name, and returns a
//pointer to the directory stream. The stream is positioned at the first entry in the directory.
//即根据路径打开一个目录,返回一个目录流指针,指针指向目录流中的第一个项目

然后通过readdir返回目录流所指向文件的信息,readdir函数原型如下:

struct dirent *readdir(DIR *dirp);
struct dirent {
ino_t d_ino; /* Inode number */
off_t d_off; /* Not an offset; see below */
unsigned short d_reclen; /* Length of this record */
unsigned char d_type; /* Type of file; not supported by all filesystem types */
char d_name[256]; /* Null-terminated filename */
};

结构体中我们只需要关注d_name,即关注文件的名字,通过文件的名字可以获取文件的状态信息,stat函数原型如下:

int stat(const char *pathname, struct stat *statbuf);
int fstat(int fd, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf); struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* Inode number */
mode_t st_mode; /* File type and mode */
nlink_t st_nlink; /* Number of hard links */
uid_t st_uid; /* User ID of owner */
gid_t st_gid; /* Group ID of owner */
dev_t st_rdev; /* Device ID (if special file) */
off_t st_size; /* Total size, in bytes */
blksize_t st_blksize; /* Block size for filesystem I/O */
blkcnt_t st_blocks; /* Number of 512B blocks allocated */ /* Since Linux 2.6, the kernel supports nanosecond
precision for the following timestamp fields.
For the details before Linux 2.6, see NOTES. */ struct timespec st_atim; /* Time of last access */
struct timespec st_mtim; /* Time of last modification */
struct timespec st_ctim; /* Time of last status change */ #define st_atime st_atim.tv_sec /* Backward compatibility */
#define st_mtime st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};

主要就是通过stat结构体来获取要发送的信息!!!

  • st_mode:保存文件类型及权限位,以及可以获取符号链接文件指向的源文件
  • st_size:文件大小
  • st_mtime:文件最后的修改时间

下面一一介绍:

获取文件类型及权限位

通过lstat函数获取文件的状态信息,然后根据statbuf.st_mode判断文件的类型与权限位,通过与宏定义相与的结果来判断。

得到的结果用一个数组来容纳,最后将每个部分的信息格式化到一个字符串中,发送字符串,获取文件类型与权限位的代码如下:

char perms[] = "----------"; //获取文件类型以及权限位 十个字符
mode_t mode = statbuf.st_mode; //statbuf.st_mode 中保存文件类型以及权限位
switch (mode & S_IFMT) {
case S_IFREG:perms[0] = '-'; break;
case S_IFDIR:perms[0] = 'd'; break;
case S_IFBLK:perms[0] = 'b'; break;
case S_IFLNK:perms[0] = 'l'; break;
case S_IFCHR:perms[0] = 'c'; break;
case S_IFSOCK:perms[0] = 's';break;
case S_IFIFO:perms[0] = 'p'; break;
default:break;
}
if (mode & S_IRUSR) perms[1] = 'r';
if (mode & S_IWUSR) perms[2] = 'w';
if (mode & S_IXUSR) perms[3] = 'x';
if (mode & S_IRGRP) perms[4] = 'r';
if (mode & S_IWGRP) perms[5] = 'w';
if (mode & S_IXGRP) perms[6] = 'x';
if (mode & S_IROTH) perms[7] = 'r';
if (mode & S_IWOTH) perms[8] = 'w';
if (mode & S_IXOTH) perms[9] = 'x';
// special perms
if (mode & S_ISUID) perms[3] = (perms[3] == 'x' ? 's' : 'S');
if (mode & S_ISGID) perms[6] = (perms[6] == 'x' ? 's' : 'S');
if (mode & S_ISVTX) perms[9] = (perms[9] == 'x' ? 's' : 'S');

获取连接数、uid、gid、文件大小

都是通过stat结构体直接获取:

char buf[1024] = {0};  //每次都要重新初始化
off = 0;
off += sprintf(buf, "%s ", perms); //添加文件类型 权限位
off += sprintf(buf + off, "%3ld %-8d %-8d ", statbuf.st_nlink, statbuf.st_uid, statbuf.st_gid); //连接数、uid、gid
off += sprintf(buf + off, "%8lu ", (unsigned long)statbuf.st_size); //添加文件大小

所有文件的信息都放在buf数组中,根据off来决定下一中属性存放的位置。

获取时间

先分析一下FTP中日期的格式:

drwxr-xr-x    3 1000     1000         4096 Feb 02 11:37 Desktop
drwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Documents
drwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Downloads
drwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Music
drwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Pictures
drwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Public
drwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Templates
drwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Videos
-rw-r--r-- 1 1000 1000 8980 Mar 21 2020 examples.desktop
drwxrwxr-x 9 1000 1000 4096 Feb 06 10:04 learn
-rw-r--r-- 1 1000 1000 2193 Mar 28 2020 vimrc

日期分为两种格式:

//如果文件时间新
drwxr-xr-x 3 1000 1000 4096 Feb 02 11:37 Desktop
//如果文件时间旧 或者是半年之前的文件
drwxr-xr-x 2 1000 1000 4096 Mar 21 2020 Documents

首先要获取当前系统的时间,和文件最后一次修改时间进行比较,判断文件的格式

获取当前时间可以通过gettimeofday:

int gettimeofday(struct timeval *tv, struct timezone *tz); //tz为NULL表示当前系统时区

The functions gettimeofday() and settimeofday() can get and set the time as well as a timezone.  The tv argument is a struct timeval (as specified in <sys/time.h>):
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
}; and gives the number of seconds and microseconds since the Epoch (see time(2)). The tz argument is a struct timezone:
struct timezone {
int tz_minuteswest; /* minutes west of Greenwich */
int tz_dsttime; /* type of DST correction */
};

然后和stat中的struct timespec st_atim; /* Time of last access */比较

如果文件时间比系统时间大,系统文件比文件时间早半年 表示文件是旧的,采用如下格式:

drwxr-xr-x    2 1000     1000         4096 Mar 21  2020 Documents
p_date_format = “%b %e %Y”;

通过调用size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);格式化时间,但是需要一个struct tm *tm,需要将秒转换为结构体的形式,通过localtime:

size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);  //格式化时间
struct tm *localtime(const time_t *timep); //将秒 转换为struct tm

代码如下:

//获取时间
char date_buf[64] = {0};
const char *p_date_format = "%b %e %H:%M";
struct timeval tv;
gettimeofday(&tv, NULL);
time_t local_time = tv.tv_sec;
if (statbuf.st_mtime > local_time || (local_time - statbuf.st_mtime) > 60*60*24*182)
p_date_format = "%b %e %Y";
struct tm *p_tm = localtime(&local_time);
strftime(date_buf, sizeof(date_buf), p_date_format, p_tm); //将时间按照对应格式格式化为字符串
off += sprintf(buf + off, "%s ", date_buf);

获取文件名

文件名的获取时要注意,符号链接文件要显式出与指向文件的关系,使用readlink函数获取实际指向的文件,将指向的文件保存在buf中。

所以符号链接文件和一般文件要分开获取文件名:

//获取文件名  符号链接文件要显式指向源文件
if (S_ISLNK(statbuf.st_mode)) {
char tmp[1024] = {0};
readlink(dt->d_name, tmp, sizeof(tmp));
sprintf(buf + off, "%s -> %s\r\n", dt->d_name, tmp);
} else {
sprintf(buf + off, "%s\r\n", dt->d_name);
}

整体融合

在获取到各个部分的信息之后,整合在buf中,通过writen发送给客户端,如下:

int list_common(session_t *sess, int detail)
{
DIR *dir = opendir("./"); //打开当前目录
struct dirent *dt; //从目录中获取文件
struct stat statbuf; //获取文件信息
int off = 0; //在整合的时候记录位置 if (dir == NULL) return 0;
//根据readdir遍历目录 使用lstat获取文件状态信息
//这里使用lstat,就是在符号链接文件的情况 查看链接文件的状态,而不是产看源文件 if (detail == 1) {
while ((dt = readdir(dir)) != NULL) {
if (lstat(dt->d_name, &statbuf) < 0 || dt->d_name[0] == '.') { //获取文件状态信息
continue;
} char perms[] = "----------"; //获取文件类型以及权限位 十个字符
mode_t mode = statbuf.st_mode; //statbuf.st_mode 中保存文件类型以及权限位
switch (mode & S_IFMT) {
case S_IFREG:perms[0] = '-'; break;
case S_IFDIR:perms[0] = 'd'; break;
case S_IFBLK:perms[0] = 'b'; break;
case S_IFLNK:perms[0] = 'l'; break;
case S_IFCHR:perms[0] = 'c'; break;
case S_IFSOCK:perms[0] = 's';break;
case S_IFIFO:perms[0] = 'p'; break;
default:break;
}
if (mode & S_IRUSR) perms[1] = 'r';
if (mode & S_IWUSR) perms[2] = 'w';
if (mode & S_IXUSR) perms[3] = 'x';
if (mode & S_IRGRP) perms[4] = 'r';
if (mode & S_IWGRP) perms[5] = 'w';
if (mode & S_IXGRP) perms[6] = 'x';
if (mode & S_IROTH) perms[7] = 'r';
if (mode & S_IWOTH) perms[8] = 'w';
if (mode & S_IXOTH) perms[9] = 'x';
// special perms
if (mode & S_ISUID) perms[3] = (perms[3] == 'x' ? 's' : 'S');
if (mode & S_ISGID) perms[6] = (perms[6] == 'x' ? 's' : 'S');
if (mode & S_ISVTX) perms[9] = (perms[9] == 'x' ? 's' : 'S'); char buf[1024] = {0}; //每次都要重新初始化
off = 0;
off += sprintf(buf, "%s ", perms); //添加文件类型 权限位
off += sprintf(buf + off, "%3ld %-8d %-8d ", statbuf.st_nlink, statbuf.st_uid, statbuf.st_gid); //左对齐 添加连接数、uid、gid
off += sprintf(buf + off, "%8lu ", (unsigned long)statbuf.st_size); //添加文件大小
//获取时间
char date_buf[64] = {0};
const char *p_date_format = "%b %e %H:%M";
struct timeval tv;
gettimeofday(&tv, NULL);
time_t local_time = tv.tv_sec;
if (statbuf.st_mtime > local_time || (local_time - statbuf.st_mtime) > 60*60*24*182)
p_date_format = "%b %e %Y";
struct tm *p_tm = localtime(&local_time);
strftime(date_buf, sizeof(date_buf), p_date_format, p_tm); //将时间按照对应格式格式化为字符串
off += sprintf(buf + off, "%s ", date_buf);
//获取文件名 符号链接文件要显式指向源文件
if (S_ISLNK(statbuf.st_mode)) {
char tmp[1024] = {0};
readlink(dt->d_name, tmp, sizeof(tmp));
sprintf(buf + off, "%s -> %s\r\n", dt->d_name, tmp);
} else {
sprintf(buf + off, "%s\r\n", dt->d_name);
}
//通过sess中的数据通道socket发送
writen(sess->data_fd, buf, strlen(buf));
} } else {
while ((dt = readdir(dir)) != NULL) {
if (lstat(dt->d_name, &statbuf) < 0 || dt->d_name[0] == '.') { //获取文件状态信息
continue;
}
char buf[1024] = {0}; //每次都要重新初始化
//获取文件名 符号链接文件要显式指向源文件
if (S_ISLNK(statbuf.st_mode)) {
char tmp[1024] = {0};
readlink(dt->d_name, tmp, sizeof(tmp));
sprintf(buf, "%s -> %s\r\n", dt->d_name, tmp);
} else {
sprintf(buf, "%s\r\n", dt->d_name);
}
//通过sess中的数据通道socket发送
writen(sess->data_fd, buf, strlen(buf));
}
}
closedir(dir);
return 1;
}

其中detail参数表示是否发送文件的详细信息。

miniFTP项目实战四的更多相关文章

  1. miniFTP项目实战五

    项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...

  2. miniFTP项目实战六

    项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...

  3. miniFTP项目实战三

    项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...

  4. miniFTP项目实战二

    项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...

  5. React-Native 之 项目实战(四)

    前言 本文有配套视频,可以酌情观看. 文中内容因各人理解不同,可能会有所偏差,欢迎朋友们联系我. 文中所有内容仅供学习交流之用,不可用于商业用途,如因此引起的相关法律法规责任,与我无关. 如文中内容对 ...

  6. miniFTP项目集合

    项目简介 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务进 ...

  7. miniFTP项目实战一

    项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...

  8. 【WEB API项目实战干货系列】- API访问客户端(WebApiClient适用于MVC/WebForms/WinForm)(四)

    这几天没更新主要是因为没有一款合适的后端框架来支持我们的Web API项目Demo, 所以耽误了几天, 目前最新的代码已经通过Sqlite + NHibernate + Autofac满足了我们基本的 ...

  9. 【.NET Core项目实战-统一认证平台】第十四章 授权篇-自定义授权方式

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章我介绍了如何强制令牌过期的实现,相信大家对IdentityServer4的验证流程有了更深的了解,本篇我将介绍如何使用自定义的授权方 ...

随机推荐

  1. linux学习之路第七天(时间日期类指令详解)

    时间日期类 1.date指令 date指令 - 显示当前日期 基本语法 1)date (功能描述:显示当前时间): 2) date + %Y (功能描述:显示当前年份) 3)date+%m( 功能描述 ...

  2. mysql,mongodb,redis区别

    MongoDB: 它是一个内存数据库,数据都是放在内存里面的. 对数据的操作大部分都在内存中,但 MongoDB 并不是单纯的内存数据库. MongoDB 是由 C++ 语言编写的,是一个基于分布式文 ...

  3. Kotlin Coroutine(协程): 三、了解协程

    @ 目录 前言 一.协程上下文 1.调度器 2.给协程起名 3.局部变量 二.启动模式 CoroutineStart 三.异常处理 1.异常测试 2.CoroutineExceptionHandler ...

  4. 全彩LED灯

    1.全彩 LED 灯,实质上是一种把红.绿.蓝单色发光体集成到小面积区域中的 LED 灯,控制时对这三种颜色的灯管输出不同的光照强度,即可混合得到不同的颜色,其混色原理与光的三原色混合原理一致.例如, ...

  5. [zebra源码]分片语句ShardPreparedStatement执行过程

    主要过程包括: 分库分表的路由定位 sql语句的 ast 抽象语法树的解析 通过自定义 SQLASTVisitor (MySQLSelectASTVisitor) 遍历sql ast,解析出逻辑表名 ...

  6. java基础---泛型机制

    从java5 开始增加泛型机制,用于明确集合中可以放入的元素类型,只在编译时期有效,运行时不区分是什么类型. 格式:<数据类型> 泛型的本质是参数化类型,让数据类型作为参数传递,E相当于形 ...

  7. Django基础011-form&modelform

    1.form from django import forms from django.core.exceptions import ValidationError #出现异常时用的 from use ...

  8. VScode中LeetCode插件无法登录的情况

    VScode中LeetCode插件无法登录的情况 一直提示账户和密码无效,不知道什么问题. 后来发现是设置问题 在插件中找到leetcode 右键,点击setting 在界面里找到这里,将leetco ...

  9. Markdown 样式美化大全

    Markdown 样式大全 目录 Markdown 样式大全 1. 键盘 2. 路径 3. 彩色字体背景 4. 折叠 5. 锚点链接 原生锚点1 原生锚点2 Hello Hello 6. 待办列表 7 ...

  10. YsoSerial 工具常用Payload分析之CC3(二)

    这是CC链分析的第二篇文章,我想按着common-collections的版本顺序来介绍,所以顺序为 cc1.3.5.6.7(common-collections 3.1),cc2.4(common- ...