SMB 协议的使用(原创)

1、什么是 SMB

SMB(Server Message Block)通信协议是微软(Microsoft)和英特尔(Intel)在1987年制定的协议,主要是作为Microsoft网络的通讯协议。SMB 是在会话层(session layer)和表示层(presentation layer)以及小部分应用层(application layer)的协议。

SMB使用了NetBIOS的应用程序接口 (Application Program Interface,简称API)。另外,它是一个开放性的协议,允许了协议扩展——使得它变得更大而且复杂;大约有65个最上层的作业,而每个作业都超过120个函数,甚至Windows NT也没有全部支持到,最近微软又把 SMB 改名为 CIFS(Common Internet File System),并且加入了许多新的特色。

2、为什么使用 SMB 协议实现文件的读取

其实 SMB 协议使用非常简单,它方便了我们对共享文件(夹)的操作,我们不在需要知道具体的盘符,只要知道 IP(地址) PORT(端口) Folder(共享文件夹)即可。

3、SMB协议的两种连接方式

  • 1、访问和验证登录信息写在一起
    1
    smb://{user}:{password}@{host}/{path}

参数说明:

参数 含义
user 用户名(一般都是登录服务器所需的用户名)
password 密码(一般都是登录服务器所需的密码,和用户名配套)
host 服务器ip
path 要访问的资源名

eg:

1
2
String url="smb://hanfeng:123@192.168.0.22/share";
SmbFile smbFile = new SmbFile(url);

注意:这种方式存在一个坑,当你的用户名或者密码中存在特殊字符时,解析会有问题,
比如像我们通用的密码会有个 @ 符号,但是在 SmbFile 解析时,这个 @ 符号后面跟的应该是 host,这样就导致他解析的结果和我们实际想要连接的不符。
那么它是怎们解析的url呢?
SmbFile 中包含的方法:

2

3

4

SmbFile 我们所使用的构造器的源码:

1
2
3
public SmbFile( String url ) throws MalformedURLException {
this( new URL( null, url, Handler.SMB_HANDLER ));
}

URL 源码:

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
107
108
109
110
111
112
113
114
115
116
117
118
119
public URL(URL context, String spec, URLStreamHandler handler)
throws MalformedURLException
{
String original = spec;
int i, limit, c;
int start = 0;
String newProtocol = null;
boolean aRef=false;
boolean isRelative = false;

// Check for permission to specify a handler
if (handler != null) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkSpecifyHandler(sm);
}
}

try {
limit = spec.length();
while ((limit > 0) && (spec.charAt(limit - 1) <= ' ')) {
limit--; //eliminate trailing whitespace
}
while ((start < limit) && (spec.charAt(start) <= ' ')) {
start++; // eliminate leading whitespace
}

if (spec.regionMatches(true, start, "url:", 0, 4)) {
start += 4;
}
if (start < spec.length() && spec.charAt(start) == '#') {
/* we're assuming this is a ref relative to the context URL.
* This means protocols cannot start w/ '#', but we must parse
* ref URL's like: "hello:there" w/ a ':' in them.
*/
aRef=true;
}
for (i = start ; !aRef && (i < limit) &&
((c = spec.charAt(i)) != '/') ; i++) {
if (c == ':') {

String s = spec.substring(start, i).toLowerCase();
if (isValidProtocol(s)) {
newProtocol = s;
start = i + 1;
}
break;
}
}

// Only use our context if the protocols match.
protocol = newProtocol;
if ((context != null) && ((newProtocol == null) ||
newProtocol.equalsIgnoreCase(context.protocol))) {
// inherit the protocol handler from the context
// if not specified to the constructor
if (handler == null) {
handler = context.handler;
}

// If the context is a hierarchical URL scheme and the spec
// contains a matching scheme then maintain backwards
// compatibility and treat it as if the spec didn't contain
// the scheme; see 5.2.3 of RFC2396
if (context.path != null && context.path.startsWith("/"))
newProtocol = null;

if (newProtocol == null) {
protocol = context.protocol;
authority = context.authority;
userInfo = context.userInfo;
host = context.host;
port = context.port;
file = context.file;
path = context.path;
isRelative = true;
}
}

if (protocol == null) {
throw new MalformedURLException("no protocol: "+original);
}

// Get the protocol handler if not specified or the protocol
// of the context could not be used
if (handler == null &&
(handler = getURLStreamHandler(protocol)) == null) {
throw new MalformedURLException("unknown protocol: "+protocol);
}

this.handler = handler;

i = spec.indexOf('#', start);
if (i >= 0) {
ref = spec.substring(i + 1, limit);
limit = i;
}

/*
* Handle special case inheritance of query and fragment
* implied by RFC2396 section 5.2.2.
*/
if (isRelative && start == limit) {
query = context.query;
if (ref == null) {
ref = context.ref;
}
}

handler.parseURL(this, spec, start, limit);

} catch(MalformedURLException e) {
throw e;
} catch(Exception e) {
MalformedURLException exception = new MalformedURLException(e.getMessage());
exception.initCause(e);
throw exception;
}
}

parseURL 源码:

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
protected void parseURL(URL u, String spec, int start, int limit) {
// These fields may receive context content if this was relative URL
String protocol = u.getProtocol();
String authority = u.getAuthority();
String userInfo = u.getUserInfo();
String host = u.getHost();
int port = u.getPort();
String path = u.getPath();
String query = u.getQuery();

// This field has already been parsed
String ref = u.getRef();

boolean isRelPath = false;
boolean queryOnly = false;

// FIX: should not assume query if opaque
// Strip off the query part
if (start < limit) {
int queryStart = spec.indexOf('?'); // 这里——hf
queryOnly = queryStart == start;
if ((queryStart != -1) && (queryStart < limit)) {
query = spec.substring(queryStart+1, limit);
if (limit > queryStart)
limit = queryStart;
spec = spec.substring(0, queryStart);
}
}

int i = 0;
// Parse the authority part if any
boolean isUNCName = (start <= limit - 4) &&
(spec.charAt(start) == '/') &&
(spec.charAt(start + 1) == '/') &&
(spec.charAt(start + 2) == '/') &&
(spec.charAt(start + 3) == '/'); // 这里——hf
if (!isUNCName && (start <= limit - 2) && (spec.charAt(start) == '/') &&
(spec.charAt(start + 1) == '/')) {
start += 2;
i = spec.indexOf('/', start);
if (i < 0) {
i = spec.indexOf('?', start);
if (i < 0)
i = limit;
}

host = authority = spec.substring(start, i);

int ind = authority.indexOf('@'); // 这里——hf
if (ind != -1) {
userInfo = authority.substring(0, ind);
host = authority.substring(ind+1);
} else {
userInfo = null;
}
if (host != null) {
// If the host is surrounded by [ and ] then its an IPv6
// literal address as specified in RFC2732
if (host.length()>0 && (host.charAt(0) == '[')) {
if ((ind = host.indexOf(']')) > 2) {

String nhost = host ;
host = nhost.substring(0,ind+1);
if (!IPAddressUtil.
isIPv6LiteralAddress(host.substring(1, ind))) {
throw new IllegalArgumentException(
"Invalid host: "+ host);
}

port = -1 ;
if (nhost.length() > ind+1) {
if (nhost.charAt(ind+1) == ':') {
++ind ;
// port can be null according to RFC2396
if (nhost.length() > (ind + 1)) {
port = Integer.parseInt(nhost.substring(ind+1));
}
} else {
throw new IllegalArgumentException(
"Invalid authority field: " + authority);
}
}
} else {
throw new IllegalArgumentException(
"Invalid authority field: " + authority);
}
} else {
ind = host.indexOf(':');
port = -1;
if (ind >= 0) {
// port can be null according to RFC2396
if (host.length() > (ind + 1)) {
port = Integer.parseInt(host.substring(ind + 1));
}
host = host.substring(0, ind);
}
}
} else {
host = "";
}
if (port < -1)
throw new IllegalArgumentException("Invalid port number :" +
port);
start = i;
// If the authority is defined then the path is defined by the
// spec only; See RFC 2396 Section 5.2.4.
if (authority != null && authority.length() > 0)
path = "";
}

if (host == null) {
host = "";
}

// Parse the file path if any
if (start < limit) {
if (spec.charAt(start) == '/') {
path = spec.substring(start, limit);
} else if (path != null && path.length() > 0) {
isRelPath = true;
int ind = path.lastIndexOf('/');
String seperator = "";
if (ind == -1 && authority != null)
seperator = "/";
path = path.substring(0, ind + 1) + seperator +
spec.substring(start, limit);

} else {
String seperator = (authority != null) ? "/" : "";
path = seperator + spec.substring(start, limit);
}
} else if (queryOnly && path != null) {
int ind = path.lastIndexOf('/');
if (ind < 0)
ind = 0;
path = path.substring(0, ind) + "/";
}
if (path == null)
path = "";

if (isRelPath) {
// Remove embedded /./
while ((i = path.indexOf("/./")) >= 0) {
path = path.substring(0, i) + path.substring(i + 2);
}
// Remove embedded /../ if possible
i = 0;
while ((i = path.indexOf("/../", i)) >= 0) {
/*
* A "/../" will cancel the previous segment and itself,
* unless that segment is a "/../" itself
* i.e. "/a/b/../c" becomes "/a/c"
* but "/../../a" should stay unchanged
*/
if (i > 0 && (limit = path.lastIndexOf('/', i - 1)) >= 0 &&
(path.indexOf("/../", limit) != 0)) {
path = path.substring(0, limit) + path.substring(i + 3);
i = 0;
} else {
i = i + 3;
}
}
// Remove trailing .. if possible
while (path.endsWith("/..")) {
i = path.indexOf("/..");
if ((limit = path.lastIndexOf('/', i - 1)) >= 0) {
path = path.substring(0, limit+1);
} else {
break;
}
}
// Remove starting .
if (path.startsWith("./") && path.length() > 2)
path = path.substring(2);

// Remove trailing .
if (path.endsWith("/."))
path = path.substring(0, path.length() -1);
}

setURL(u, protocol, host, port, authority, userInfo, path, query, ref);
}

可以看到上面注释 “这里——hf” 的地方,特殊符号是写死的。这就是为什么特殊字符会有坑的存在。

  • 2 访问和验证登录信息分开
    分别定义 userInfo 和 url。
    eg:
    1
    2
    3
    4
    5
    6
    String userName = "hanfeng";
    String passWord = "123";
    String domainIp = "172.16.192.156";
    String url = "Smb://172.16.192.156/share";
    NtlmPasswordAuthentication auth = new NtlmPasswordAuthentication(domainIp, userName, passWord);
    SmbFile smbFile = new SmbFile(url, auth);

构造器源码:

1
2
3
4
public SmbFile( String url, NtlmPasswordAuthentication auth )
throws MalformedURLException {
this( new URL( null, url, Handler.SMB_HANDLER ), auth );
}

this调用的源:

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
public SmbFile( URL url, NtlmPasswordAuthentication auth ) {
super( url );
this.auth = auth == null ? new NtlmPasswordAuthentication( url.getUserInfo() ) : auth;

getUncPath0();
}
SmbFile( SmbFile context, String name, int type,
int attributes, long createTime, long lastModified, long size )
throws MalformedURLException, UnknownHostException {
this( context.isWorkgroup0() ?
new URL( null, "smb://" + name + "/", Handler.SMB_HANDLER ) :
new URL( context.url, name + (( attributes & ATTR_DIRECTORY ) > 0 ? "/" : "" )));

/* why was this removed before? DFS? copyTo? Am I going around in circles? */
auth = context.auth;


if( context.share != null ) {
this.tree = context.tree;
this.dfsReferral = context.dfsReferral;
}
int last = name.length() - 1;
if( name.charAt( last ) == '/' ) {
name = name.substring( 0, last );
}
if( context.share == null ) {
this.unc = "\\";
} else if( context.unc.equals( "\\" )) {
this.unc = '\\' + name;
} else {
this.unc = context.unc + '\\' + name;
}
/* why? am I going around in circles?
* this.type = type == TYPE_WORKGROUP ? 0 : type;
*/
this.type = type;
this.attributes = attributes;
this.createTime = createTime;
this.lastModified = lastModified;
this.size = size;
isExists = true;

attrExpiration = sizeExpiration =
System.currentTimeMillis() + attrExpirationPeriod;
}

4、实现文件的读写

那么上面我们已将连接好了我们想要连接的服务器目录,如何实现文件的读写呢?
要实现读写要有个前提,就是在目标服务器上给我们当前使用用户授予读写的权限,否则是会出问题的。

一般我们读取文件都是用 FileInputStream,那么smbFile可不可以使用 FileInputStream来读取呢?
6

那么怎么解决?????

在 InputStream 下有个子类(非 jdk 提供,或者可以说是个人实现的),叫 SmbFileInputStream
我们来试下这个是不是好用:
7
没有问题,是可以的,我们来看下 SmbFileInputStream 的实现:

8
我们可以清楚的看到 SmbFileInputStream 实际上也是继承了 InputStream
这样我们就可以根绝我们熟悉的 InputStream 去操作流了。

但是 SmbFile 不能用并发来实现,因为没有意义,他的底层全部使用了 synchronized ,即使你用并发,也是放在队列里,一条一条处理。想了解细节的可以去看下源码,这里就不过多说了。

5、最后附上完整的读写示例代码:

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
 
package com.thunisoft.fy.dzjz.fiveimport.service;

import java.io.InputStream;
import java.io.OutputStream;

import jcifs.smb.NtlmPasswordAuthentication;
import jcifs.smb.SmbFile;
import jcifs.smb.SmbFileInputStream;
import jcifs.smb.SmbFileOutputStream;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;

public class SmbFileRead {

/**smb协议头*/
private final String SMB_URL_TOP = "smb://";

/**用户名*/
private final String SMB_USER = "hanfeng";

/**斜杠*/
private final String SMB_URL_SLANSH = "/";

/**密码*/
private final String SMB_PASSWORD = "123";

/**base folder*/
private final String SMB_BASE_FOLDER_NAME = "share";

/**IP*/
private String SMB_IP = "172.16.192.156";

/**url*/
private String SMB_URL = StringUtils.EMPTY;

/**用户信息*/
private NtlmPasswordAuthentication auth = null;

public void init() {
if (null == this.auth) {
this.auth = new NtlmPasswordAuthentication(SMB_IP, SMB_USER, SMB_PASSWORD);
}
if (StringUtils.isBlank(this.SMB_URL)) {
this.SMB_URL = SMB_URL_TOP + SMB_IP + SMB_URL_SLANSH + SMB_BASE_FOLDER_NAME;
}
}

/**
* 读
* @return
* @throws Exception
*/
public InputStream openFile() throws Exception {
init();
SmbFile smbFile = new SmbFile(this.SMB_URL, this.auth);
// SmbFile outSmbFile=new SmbFile(this.SMB_URL,this.auth);
// smbFile.createNewFile(); //创建文件
// smbFile.copyTo(outSmbFile); //将smbFile copy 到 outSmbFile
// smbFile.mkdirs(); //创建目录(还有 mkdir。mkdirs 支持多级创建,mkdir不支持)
// smbFile.listFiles(); //获取子文件/文件夹
return new SmbFileInputStream(smbFile);
}

public void writeFile() throws Exception {
InputStream in = openFile();
SmbFile smbFile = new SmbFile(this.SMB_URL, this.auth);//目标地址 这里我就不改了 人懒 ^V^.
/**
* 验证文件 或者文件夹是否存在 不存在先创建
*/
if (!smbFile.exists()) {
smbFile.createNewFile();
// 文件夹创建
smbFile.mkdirs();
//注:穿件文件不能包含目录创建,需要先创建目录在创建文件
}
OutputStream out = new SmbFileOutputStream(smbFile);
IOUtils.copy(in, out);
//因为 IOUtils 中没有给刷新输出流,也没有关流 ,所以需要自己来处理
out.flush();
out.close();
in.close();
/**
* 一般情况下是:先打开的后关闭,后打开的先关闭
* 另一种情况:看依赖关系,如果流a依赖流b,应该先关闭流a,再关闭流b
* 例如处理流a依赖节点流b,应该先关闭处理流a,再关闭节点流b
* 当然完全可以只关闭处理流,不用关闭节点流。处理流关闭的时候,会调用其处理的节点流的关闭方法
* 如果将节点流关闭以后再关闭处理流,会抛出IO异常
*/

}

}

转载请说明出处http://hanfeng.me/2018/01/21/SMB%20%E5%8D%8F%E8%AE%AE%E7%9A%84%E4%BD%BF%E7%94%A8%EF%BC%88%E5%8E%9F%E5%88%9B%EF%BC%89/;
欢迎拍砖。