fastdfs上傳
fastdfs是一個輕量級的分散式檔案系統,主要由 tracker server, storage server 以及client組成,這裡主要涉及兩點 :
1)客戶端上傳檔案流程和協議分析
2)實現一個簡單的檔案上傳函式
一: 檔案上傳的基本流程
fastdfs中上傳一個檔案,主要涉及以下幾個步驟:
1)上傳連線請求,客戶端會向tracker server發出上傳檔案的請求
2)tracker收到請求後,返回storage server的ip和埠
3)客戶端連線storage,並且上傳檔案
4)檔案上傳完成後,storage返回路徑資訊
以下具體分析檔案上傳過程中的協議和各種操作
fastdfs協議頭部:
typedef struct
{
char pkg_len[FDFS_PROTO_PKG_LEN_SIZE]; //body length, not including header(8個位元組)
char cmd; //command code TRACKER_PROTO_CMD_SERVICE_QUERY_STORE_WITHOUT_GROUP_ONE
char status; //status code for response
} TrackerHeader;
1
2
3
4
5
6
fastdfs協議的頭部是由10個位元組大小的結構體構成,
傳送:傳送資料時,先發送TrackerHeader到伺服器,隨後傳送具體的資料
接受:接受資料時,先接受sizeof(TrackerHeader)大小的報文頭部,隨後接受pkg_len長度的報文體
status: 傳送的時候設定為0
cmd: 命令
pkg_len:一個int64_t的整型,除去TrackerHeader長度的報文長度
二: 客戶端向tracker server傳送獲取storage地址請求
#define TRACKER_PROTO_CMD_SERVICE_QUERY_STORE_WITHOUT_GROUP_ONE 101
// 協議頭
// pkg_len | cmd | status
// 8 bytes | 1 bytes | 1 bytes
//向tracker server請求storage server cmd
#define TRACKER_PROTO_CMD_SERVICE_QUERY_STORE_WITHOUT_GROUP_ONE 101
TrackerHeader header;//協議頭部
memset(&header, 0, sizeof(TrackerHeader));
header.cmd = TRACKER_PROTO_CMD_SERVICE_QUERY_STORE_WITHOUT_GROUP_ONE;
//向tracker server 請求 storage,tcpsenddata 返回非0,表示傳送成功
if(tcpsenddata(sockfd, &header, sizeof(TrackerHeader), 10, &count) != 0)
{
fprintf(stderr, "tcpsenddata error: %s\n", strerror(errno));
return 1;
}
else//請求傳送成功,等待tracker回覆
{
//接收頭部,頭部是一個TrackerHeader型別,10個位元組
TrackerHeader resp;
if((ret_code = tcprecvdata(sockfd, &resp, sizeof(TrackerHeader), 10, &count)) != 0)
{
fprintf(stderr, "tcprecvdata error: %s\n", strerror(ret_code));
return 1;
}
//開始接收報文體
//int64_t read_int64(const char *buff)
//{
// unsigned char *p;
// p = (unsigned char *)buff;
// return (((int64_t)(*p)) << 56) | \
// (((int64_t)(*(p+1))) << 48) | \
// (((int64_t)(*(p+2))) << 40) | \
// (((int64_t)(*(p+3))) << 32) | \
// (((int64_t)(*(p+4))) << 24) | \
// (((int64_t)(*(p+5))) << 16) | \
// (((int64_t)(*(p+6))) << 8) | \
// ((int64_t)(*(p+7)));
//}
int size = read_int64(resp.pkg_len);//獲取報體長度
char *buf = (char*)calloc(size + 1, sizeof(char));
if((ret_code = tcprecvdata(sockfd, buf, size, 10, &count) != 0))
{
fprintf(stderr, "tcprecvdata error: %s\n", strerror(ret_code));
return 1;
}
// 報文體
// group_name |ip |port |storage_index
// 16 bytes |16 bytes |8 bytes |
//#define TRACKER_QUERY_STORAGE_STORE_BODY_LEN 40
if(count != TRACKER_QUERY_STORAGE_STORE_BODY_LEN)
{
fprintf(stderr, "invalid message");
return 1;
}
//group name
//#define FDFS_GROUP_NAME_MAX_LEN 16
char group_name[FDFS_GROUP_NAME_MAX_LEN + 1] = {0};
memcpy(group_name, buf, FDFS_GROUP_NAME_MAX_LEN);
group_name[FDFS_GROUP_NAME_MAX_LEN] = '\0';
//ip: port
//#define IP_ADDRESS_SIZE 16
//port:8 bytes
char ip[IP_ADDRESS_SIZE + 1] = {0};
memcpy(ip, buf + FDFS_GROUP_NAME_MAX_LEN, IP_ADDRESS_SIZE - 1);
char szPort[8] = {0};
memcpy(szPort, buf + FDFS_GROUP_NAME_MAX_LEN + IP_ADDRESS_SIZE - 1, 8);
ip[IP_ADDRESS_SIZE] = '\0';
int port = read_int64(szPort);
//storage index;
char *storage_index = buf + FDFS_GROUP_NAME_MAX_LEN + IP_ADDRESS_SIZE - 1 + FDFS_PROTO_PKG_LEN_SIZE;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
三:以上步驟完成後,獲取storage的ip 和 port後,就可以上傳檔案了
在官方的客戶端中,檔案操作有upload,download, append,delete等,這裡只涉及upload
上傳檔案中,官方給出了三種方式
1)通過buffer上傳,即將檔案讀取進記憶體,然後在傳送
2)使用sendfile,sendfile是Linux提供的一個庫函式
3)通過回撥函式的方式
這裡主要涉及的是第一種,通過buffer上傳的方式
檔案上傳協議:
//檔案上傳協議頭部
10 bytes | 1 bytes | 8 bytes | 6 bytes |
TrackerHeader | storage_index | 檔案長度 | 檔名或者全為0) |
//storage_index 是客戶端向tracker server申請storage index時候返回的結果
//檔名 如果不為空,那麼取前6位,或者可以全部設定為0
//上傳完成 storage回覆客戶端協議
10 bytes | 16 bytes | TrackerHeader.pkg_len - 16bytes
TrackerHeader | groupname | remote file name
1
2
3
4
5
6
7
8
9
10
11
12
void uploadfile(int sockfd, const char *filepath, char *storage_index)
{
char out_buf[512];
TrackerHeader *pHeader;
char *p = out_buf;
char *buf = NULL;
//TrackerHeader 10 bytes
//檔案上傳協議頭部
//10 bytes | 1 bytes | 8 bytes | 6 bytes |
//TrackerHeader | storage_index | 檔案長度 | 檔名或者全為0) |
pHeader = (TrackerHeader*)out_buf;
p += sizeof(TrackerHeader);
//storage index 1 bytes
*p++ = *storage_index;
//filesize 8bytes
long int filesize = 0;
int ret = 0;
//讀取檔案到buf,並且返回檔案長度 filesize
if((ret = getfilebuf(&buf, &filesize, filepath) != 0))
{
fprintf(stderr, "getfilebuf failed: %s\n", strerror(ret));
return;
}
//void write_int64(int64_t n, char *buff)
//{
// unsigned char *p;
// p = (unsigned char *)buff;
// *p++ = (n >> 56) & 0xFF;
// *p++ = (n >> 48) & 0xFF;
// *p++ = (n >> 40) & 0xFF;
// *p++ = (n >> 32) & 0xFF;
// *p++ = (n >> 24) & 0xFF;
// *p++ = (n >> 16) & 0xFF;
// *p++ = (n >> 8) & 0xFF;
// *p++ = n & 0xFF;
//}
write_int64(filesize, p);
//#define FDFS_PROTO_PKG_LEN_SIZE 8
p += FDFS_PROTO_PKG_LEN_SIZE;
//ext_name
//#define FDFS_FILE_EXT_NAME_MAX_LEN 6
memset(p, 0, FDFS_FILE_EXT_NAME_MAX_LEN);
p += FDFS_FILE_EXT_NAME_MAX_LEN;
//set TrackerHeader
write_int64(p - out_buf + filesize - sizeof(TrackerHeader), pHeader->pkg_len);
//#define STORAGE_PROTO_CMD_UPLOAD_FILE 11
pHeader->cmd = STORAGE_PROTO_CMD_UPLOAD_FILE;
pHeader->status = 0;
//傳送報文頭部
int count;
int ret_code = 0;
if((ret_code = tcpsenddata(sockfd, out_buf, p - out_buf, 10, &count) != 0)) {
fprintf(stderr, "tcpsenddata failed: %s\n", strerror(errno));
return;
}
//傳送報文體,具體檔案資料
if((ret_code = tcpsenddata(sockfd, buf, filesize, 10, &count)) != 0) {
fprintf(stderr, "tcpsenddata body failed: %s\n", strerror(errno));
return;
}
//接收storage server回覆
//上傳完成 storage回覆客戶端協議
//10 bytes | 16 bytes | TrackerHeader.pkg_len - 16bytes
//TrackerHeader | groupname | remote file name
TrackerHeader resp;
if((ret_code = tcprecvdata(sockfd, &resp, sizeof(TrackerHeader), 1000, &count)) != 0) {
fprintf(stderr, "tcprecvdata failed: %s\n", strerror(ret_code));
return;
}
if(count != sizeof(TrackerHeader)) {
fprintf(stderr, "invalid header");
return;
}
int64_t bodylen = read_int64(resp.pkg_len);
//接收報文體
char *in_buf = (char*)calloc(bodylen + 1, sizeof(char));
if((ret_code = tcprecvdata(sockfd, in_buf, bodylen, 10, &count)) != 0)
{
fprintf(stderr, "read body failed: %s\n", strerror(ret_code));
return;
}
//groupname
//#define FDFS_GROUP_NAME_MAX_LEN 16
char group_name[FDFS_GROUP_NAME_MAX_LEN + 1];
memcpy(group_name, in_buf, FDFS_GROUP_NAME_MAX_LEN);
group_name[FDFS_GROUP_NAME_MAX_LEN] = '\0';
//remote filename
char remote_filename[bodylen - FDFS_GROUP_NAME_MAX_LEN + 1];
memcpy(remote_filename, in_buf + FDFS_GROUP_NAME_MAX_LEN, bodylen - FDFS_GROUP_NAME_MAX_LEN + 1);
cout << "groupname: " << group_name << endl;
cout << "remote_filename: " << remote_filename << endl;
char httpaddr[128] = {0};
sprintf(httpaddr, "http://106.75.129.177:8080/%s/%s", group_name, remote_filename);
cout << "httpaddr: " << httpaddr << endl;//http地址
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
以下附上完整程式碼, ubuntu14位, 編譯器 g++,測試已通過
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
using namespace std;
#define FDFS_GROUP_NAME_MAX_LEN 16
#define FDFS_PROTO_PKG_LEN_SIZE 8
#define IP_ADDRESS_SIZE 16
//cmd
#define TRACKER_PROTO_CMD_SERVICE_QUERY_STORE_WITHOUT_GROUP_ONE 101
#define STORAGE_PROTO_CMD_UPLOAD_FILE 11
#define TRACKER_QUERY_STORAGE_STORE_BODY_LEN (FDFS_GROUP_NAME_MAX_LEN \
+ IP_ADDRESS_SIZE - 1 + FDFS_PROTO_PKG_LEN_SIZE + 1)
#define FDFS_FILE_EXT_NAME_MAX_LEN 6
typedef struct {
char pkg_len[FDFS_PROTO_PKG_LEN_SIZE];
char cmd;
char status;
}TrackerHeader;
//set socketfd nonblocking
int setnonblocking(int sockfd);
int tcprecvdata(int sockfd, void *data, const int size,\
const int timeout_ms, int *count);
int tcpsenddata(int sockfd, void *data, const int size,\
const int timeout_ms, int *count);
int64_t read_int64(const char* buf);
void write_int64(int64_t n, char* buf);
void uploadfile(int sockfd, const char *filepath, char *storage_index);
int getfilebuf(char **buf, long int *filesize, const char* filepath);
//apply storage address from tracker server
int main() {
const char *ip = "127.0.0.1";
uint16_t port = 22122;
int ret_code = 0;
int sockfd = -1;
int count = 0;
//connect tracker server
if((sockfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)
{
fprintf(stderr, "socket errnor: %s\n", strerror(errno));
return 1;
}
if((ret_code = setnonblocking(sockfd)) != 0)
{
fprintf(stderr, "setnonblocking error: %s\n", strerror(ret_code));
return 1;
}
struct sockaddr_in addr;
addr.sin_addr.s_addr = inet_addr(ip);
addr.sin_port = htons(port);
addr.sin_family = AF_INET;
socklen_t len = sizeof(struct sockaddr);
if(connect(sockfd, (struct sockaddr*)&addr, len) < 0)
{
fprintf(stderr, "connect error: %s\n", strerror(errno));
return 1;
}
TrackerHeader header;
memset(&header, 0, sizeof(TrackerHeader));
header.cmd = TRACKER_PROTO_CMD_SERVICE_QUERY_STORE_WITHOUT_GROUP_ONE;
if(tcpsenddata(sockfd, &header, sizeof(TrackerHeader), 10, &count) != 0)
{
fprintf(stderr, "tcpsenddata error: %s\n", strerror(errno));
return 1;
}
else
{
//recv header
TrackerHeader resp;
if((ret_code = tcprecvdata(sockfd, &resp, sizeof(TrackerHeader), 10, &count)) != 0)
{
fprintf(stderr, "tcprecvdata error: %s\n", strerror(ret_code));
return 1;
}
cout << "recv header: " << count << endl;
//read body;
int size = read_int64(resp.pkg_len);
char *buf = (char*)calloc(size + 1, sizeof(char));
if((ret_code = tcprecvdata(sockfd, buf, size, 10, &count) != 0))
{
fprintf(stderr, "tcprecvdata error: %s\n", strerror(ret_code));
return 1;
}
//body
// group_name |ip |port |storage_index
// 16bytes |16bytes |8bytes |
cout << "read body: " << count << endl;
if(count != TRACKER_QUERY_STORAGE_STORE_BODY_LEN)
{
fprintf(stderr, "invalid message");
return 1;
}
//group name
char group_name[FDFS_GROUP_NAME_MAX_LEN + 1] = {0};
memcpy(group_name, buf, FDFS_GROUP_NAME_MAX_LEN);
group_name[FDFS_GROUP_NAME_MAX_LEN] = '\0';
cout << "group name: " << group_name << endl;
//ip: port
char ip[IP_ADDRESS_SIZE + 1] = {0};
memcpy(ip, buf + FDFS_GROUP_NAME_MAX_LEN, IP_ADDRESS_SIZE - 1);
char szPort[8] = {0};
memcpy(szPort, buf + FDFS_GROUP_NAME_MAX_LEN + IP_ADDRESS_SIZE - 1, 8);
ip[IP_ADDRESS_SIZE] = '\0';
int port = read_int64(szPort);
cout << "address: " << ip << ":" << port << endl;
//storage index;
char *storage_index = buf + FDFS_GROUP_NAME_MAX_LEN + IP_ADDRESS_SIZE - 1 + FDFS_PROTO_PKG_LEN_SIZE;
cout << "storage_index: " << storage_index << endl;
free(buf);
//connect storage server
sockaddr_in st_addr;
st_addr.sin_addr.s_addr = inet_addr(ip);
st_addr.sin_family = AF_INET;
st_addr.sin_port = htons(port);
int storage_fd = socket(AF_INET, SOCK_STREAM, 0);
if(storage_fd < 0) {
fprintf(stderr, "socket failed: %s\n", strerror(errno));
return 1;
}
socklen_t len2 = sizeof(sockaddr_in);
if(connect(storage_fd, (struct sockaddr*)&st_addr, len2) < 0) {
fprintf(stderr, "connect failed: %s\n", strerror(errno));
return 1;
}
uploadfile(storage_fd, "1.jpg", storage_index);
}
return 0;
}
int getfilebuf(char **buf, long int *filesize, const char *filepath)
{
int ret_code = 0;
FILE *fp = fopen(filepath, "rb+");
if(fp == NULL)
{
ret_code = errno;
return ret_code;
}
//get filesize;
fseek(fp, 0, SEEK_END);
*filesize = ftell(fp);
fseek(fp, 0, SEEK_SET);
cout << "get filesize: " <<*filesize << endl;
//malloc buf
*buf = (char*)calloc(*filesize + 1, sizeof(char));
if(*buf == NULL) {
ret_code = errno;
return ret_code;
}
int read_bytes = 0;
int left_bytes = *filesize;
char *p = *buf;
while(left_bytes > 0) {
read_bytes = fread(p, sizeof(char), left_bytes, fp);
left_bytes -= read_bytes;
p += read_bytes;
}
return ret_code;
}
void uploadfile(int sockfd, const char *filepath, char *storage_index)
{
char out_buf[512];
TrackerHeader *pHeader;
char *p = out_buf;
char *buf = NULL;
//TrackerHeader 10 bytes
pHeader = (TrackerHeader*)out_buf;
p += sizeof(TrackerHeader);
//storage index 1 bytes
*p++ = *storage_index;
//filesize 8bytes
long int filesize = 0;
int ret = 0;
if((ret = getfilebuf(&buf, &filesize, filepath) != 0))
{
fprintf(stderr, "getfilebuf failed: %s\n", strerror(ret));
return;
}
printf("filesize: %ld\n", filesize);
write_int64(filesize, p);
p += FDFS_PROTO_PKG_LEN_SIZE;
//ext_name
memset(p, 0, FDFS_FILE_EXT_NAME_MAX_LEN);
p += FDFS_FILE_EXT_NAME_MAX_LEN;
//set TrackerHeader
write_int64(p - out_buf + filesize - sizeof(TrackerHeader), pHeader->pkg_len);
pHeader->cmd = STORAGE_PROTO_CMD_UPLOAD_FILE;
pHeader->status = 0;
//send header
int count;
int ret_code = 0;
if((ret_code = tcpsenddata(sockfd, out_buf, p - out_buf, 10, &count) != 0)) {
fprintf(stderr, "tcpsenddata failed: %s\n", strerror(errno));
return;
}
//send body
if((ret_code = tcpsenddata(sockfd, buf, filesize, 10, &count)) != 0) {
fprintf(stderr, "tcpsenddata body failed: %s\n", strerror(errno));
return;
}
//recv response
TrackerHeader resp;
if((ret_code = tcprecvdata(sockfd, &resp, sizeof(TrackerHeader), 1000, &count)) != 0) {
fprintf(stderr, "tcprecvdata failed: %s\n", strerror(ret_code));
return;
}
if(count != sizeof(TrackerHeader)) {
fprintf(stderr, "invalid header");
return;
}
int64_t bodylen = read_int64(resp.pkg_len);
char *in_buf = (char*)calloc(bodylen + 1, sizeof(char));
if((ret_code = tcprecvdata(sockfd, in_buf, bodylen, 10, &count)) != 0)
{
fprintf(stderr, "read body failed: %s\n", strerror(ret_code));
return;
}
//groupname
char group_name[FDFS_GROUP_NAME_MAX_LEN + 1];
memcpy(group_name, in_buf, FDFS_GROUP_NAME_MAX_LEN);
group_name[FDFS_GROUP_NAME_MAX_LEN] = '\0';
//remote filename
char remote_filename[bodylen - FDFS_GROUP_NAME_MAX_LEN + 1];
memcpy(remote_filename, in_buf + FDFS_GROUP_NAME_MAX_LEN, bodylen - FDFS_GROUP_NAME_MAX_LEN + 1);
cout << "groupname: " << group_name << endl;
cout << "remote_filename: " << remote_filename << endl;
char httpaddr[128] = {0};
sprintf(httpaddr, "http://127.0.0.1:8080/%s/%s", group_name, remote_filename);
cout << "httpaddr" << httpaddr << endl;
}
void write_int64(int64_t n, char *buff)
{
unsigned char *p;
p = (unsigned char *)buff;
*p++ = (n >> 56) & 0xFF;
*p++ = (n >> 48) & 0xFF;
*p++ = (n >> 40) & 0xFF;
*p++ = (n >> 32) & 0xFF;
*p++ = (n >> 24) & 0xFF;
*p++ = (n >> 16) & 0xFF;
*p++ = (n >> 8) & 0xFF;
*p++ = n & 0xFF;
}
int64_t read_int64(const char *buff)
{
unsigned char *p;
p = (unsigned char *)buff;
return (((int64_t)(*p)) << 56) | \
(((int64_t)(*(p+1))) << 48) | \
(((int64_t)(*(p+2))) << 40) | \
(((int64_t)(*(p+3))) << 32) | \
(((int64_t)(*(p+4))) << 24) | \
(((int64_t)(*(p+5))) << 16) | \
(((int64_t)(*(p+6))) << 8) | \
((int64_t)(*(p+7)));
}
int setnonblocking(int sockfd)
{
int ret_code = 0;
if(fcntl(sockfd, F_SETFD, O_NONBLOCK) < 0) {
ret_code = errno;
}
return ret_code;
}
int tcpsenddata(int sockfd, void *data, const int size,\
const int timeout_ms, int *count)
{
int left_bytes = size;
int write_bytes = 0;
int ret_code = 0;
int res = 0;
char *p = (char*)data;
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(sockfd, &rfds);
while(left_bytes > 0) {
write_bytes = send(sockfd, p, left_bytes, 0);
if(write_bytes > 0)
{
left_bytes -= write_bytes;
p += write_bytes;
continue;
}
else if(write_bytes < 0)
{
if(!(errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK))
{
ret_code = errno == 0 ? errno : EINTR;
break;
}
}
else
{
ret_code = ENOTCONN;
break;
}
if(timeout_ms <= 0)
{
res = select(sockfd + 1, &rfds, NULL, NULL, NULL);
}
else
{
struct timeval tv;
tv.tv_usec = timeout_ms;
tv.tv_sec = 0;
res = select(sockfd + 1, &rfds, NULL, NULL, &tv);
}
if(res == 0)
{
ret_code = ETIMEDOUT;
break;
}
if(res < 0)
{
if(errno == EINTR)
{
continue;
}
ret_code = errno == 0 ? errno : EINTR;
}
}
if(count != NULL)
{
*count = size - left_bytes;
}
return ret_code;
}
int tcprecvdata(int sockfd, void *data, const int size,\
const int timeout_ms, int *count) {
int left_bytes = size;
int read_bytes = 0;
int ret_code = 0;
int res = 0;
char *p = (char*)data;
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(sockfd, &rfds);
while(left_bytes > 0) {
read_bytes = recv(sockfd, p, left_bytes, 0);
if(read_bytes > 0)
{
left_bytes -= read_bytes;
p += read_bytes;
continue;
}
else if(read_bytes < 0)
{
if(!(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR))
{
ret_code = errno != 0 ? errno : EINTR;
break;
}
}
else
{
ret_code = ENOTCONN;
break;
}
if(timeout_ms <= 0)
{
res = select(sockfd + 1, &rfds, NULL, NULL, NULL);
}
else
{
struct timeval tv;
tv.tv_usec = timeout_ms;
tv.tv_sec = 0;
res = select(sockfd + 1, &rfds, NULL, NULL, &tv);
}
if(res == 0)
{
ret_code = ETIMEDOUT;
break;
}
if(res < 0)
{
if(errno == EINTR)
{
continue;
}
ret_code = errno == 0 ? errno : EINTR;
break;
}
}
if(count != NULL)
{
*count = size - left_bytes;
}
return ret_code;
---------------------
作者:Ada_baby
來源:CSDN
原文:https://blog.csdn.net/Ada_baby/article/details/51274157
版權宣告:本文為博主原創文章,轉載請附上博文連結!