项目简介:
在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. SpringMVC(7)格式化显示

    在SpringMVC(六)数据验证中我们介绍了如何验证提交的数据的正确性,当数据验证通过后就会被我们保存起来.保存的数据会用于以后的展示,这才是保存的价值.那么在展示的时候如何按照要求显示?(比如:小 ...

  2. 浅谈C++11中的多线程(三)

    摘要 本篇文章围绕以下几个问题展开: 进程和线程的区别 何为并发?C++中如何解决并发问题?C++中多线程的基本操作 浅谈C++11中的多线程(一) - 唯有自己强大 - 博客园 (cnblogs.c ...

  3. FreeRTOS+LVGL|Freertos+lvgl如何配置lvgl的心跳和任务管理器

    目录 配置lvgl心跳(Tick) 配置lvgl任务管理器(Task Handler) LVGL中文手册 lvgl需要系统滴答声(心跳)才能知道动画和其他任务的经过时间,所以我们必须要配置好lvgl的 ...

  4. C语言:fopen

    fopen,传递文件名参数,w+选项读取用fread或fgets,其中fread是按字节读取,fgets每次读取一个字符串写入用fwrite或fputs或fprintf,fwrite按字节写入,fpu ...

  5. win10 sql2008r2网页不能使用数据,需要开启端口1433

    1.打开sql server configuratiton Manager 2.sqlserver网络配置--SQLEXPRESS的协议:都启用 3.双击TCP/IP:选择"IP地址&quo ...

  6. SOA-面向服务的架构

    一.什么是SOA? SOA 面向服务架构,是一个架构思想,是跨语言和平台的.SOA宗旨简单明了,根据项目服务完成架构搭建,以服务为基准点完成组件化和模块化.提供服务是项目的基本内容,其他的contro ...

  7. 使用deepin系统自带命令切割视频使用lossless免费软件切割

    1,使用ffmpeg命令:ffmpeg -ss 00:00:10 -i gaodengshuxue.mp4 -vcodec copy -acodec copy -t 00:00:20 output.m ...

  8. 流暢的python---函數闭包

    一.函数的定义及其应用所谓函数,就是把具有独立功能的代码块组织成为一个小模块,在需要的时候调用函数的使用包含两个步骤1.定义函数–封装独立的功能2.调用函数–享受封装的成果函数的作用:在开发时,使用函 ...

  9. Python开发篇——如何在Flask下编写JWT登录

    首先,HTTP 是无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息)--每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求 ...

  10. idea创建web工程、配置tomcat及基本配置

    背景 现在许多人从eclipse转到idea了,小编也不例外.但是刚转初期还是有挺多不适应的,特总结了创建maven的web工程.配置tomcat服务器及基本配置.有了这些配置,上手idea也就跟ec ...