按部就班Socket網絡編程(多客戶端、信息共享、文件傳輸)java
前言:在最近一個即將結束的項目中使用到了Socket編程,用於調用另外一系統進行處理並返回數據。故把Socket的基礎知識總結梳理一遍。編程
一、TCP/IP協議緩存
既然是網絡編程,涉及幾個系統之間的交互,那麼首先要考慮的是如何準確的定位到網絡上的一臺或幾臺主機,另外一個是如何進行可靠高效的數據傳輸。這裏就要使用到TCP/IP協議。服務器
TCP/IP協議(傳輸控制協議)由網絡層的IP協議和傳輸層的TCP協議組成。IP層負責網絡主機的定位,數據傳輸的路由,由IP地址能夠惟一的肯定Internet上的一臺主機。TCP層負責面向應用的可靠的或非可靠的數據傳輸機制,這是網絡編程的主要對象。網絡
二、TCP與UDPsocket
TCP是一種面向鏈接的保證可靠傳輸的協議。經過TCP協議傳輸,獲得的是一個順序的無差錯的數據流。發送方和接收方的成對的兩個socket之間必須創建鏈接,以便在TCP協議的基礎上進行通訊,當一個socket(一般都是server socket)等待創建鏈接時,另外一個socket能夠要求進行鏈接,一旦這兩個socket鏈接起來,它們就能夠進行雙向數據傳輸,雙方均可以進行發送或接收操做。ide
UDP是一種面向無鏈接的協議,每一個數據報都是一個獨立的信息,包括完整的源地址或目的地址,它在網絡上以任何可能的路徑傳往目的地,所以可否到達目的地,到達目的地的時間以及內容的正確性都是不能被保證的。測試
TCP與UDP區別:大數據
TCP特色:this
UDP特色:
TCP與UDP應用:
三、Socket是什麼
Socket一般也稱做"套接字",用於描述IP地址和端口,是一個通訊鏈的句柄。網絡上的兩個程序經過一個雙向的通信鏈接實現數據的交換,這個雙向鏈路的一端稱爲一個Socket,一個Socket由一個IP地址和一個端口號惟一肯定。應用程序一般經過"套接字"向網絡發出請求或者應答網絡請求。 Socket是TCP/IP協議的一個十分流行的編程界面,可是,Socket所支持的協議種類也不光TCP/IP一種,所以二者之間是沒有必然聯繫的。在Java環境下,Socket編程主要是指基於TCP/IP協議的網絡編程。
Socket通信過程:服務端監聽某個端口是否有鏈接請求,客戶端向服務端發送鏈接請求,服務端收到鏈接請求向客戶端發出接收消息,這樣一個鏈接就創建起來了。客戶端和服務端均可以相互發送消息與對方進行通信。
Socket的基本工做過程包含如下四個步驟:
四、Java中的Socket
在java.net包下有兩個類:Socket和ServerSocket。ServerSocket用於服務器端,Socket是創建網絡鏈接時使用的。在鏈接成功時,應用程序兩端都會產生一個Socket實例,操做這個實例,完成所需的會話。對於一個網絡鏈接來講,套接字是平等的,並無差異,不由於在服務器端或在客戶端而產生不一樣級別。不論是Socket仍是ServerSocket它們的工做都是經過SocketImpl類及其子類完成的。
列出幾個經常使用的構造方法:
1
2
3
4
5
6
7
8
9
|
Socket(InetAddress address,
int
port);
//建立一個流套接字並將其鏈接到指定 IP 地址的指定端口號
Socket(String host,
int
port);
//建立一個流套接字並將其鏈接到指定主機上的指定端口號
Socket(InetAddress address,
int
port, InetAddress localAddr,
int
localPort);
//建立一個套接字並將其鏈接到指定遠程地址上的指定遠程端口
Socket(String host,
int
port, InetAddress localAddr,
int
localPort);
//建立一個套接字並將其鏈接到指定遠程主機上的指定遠程端口
Socket(SocketImpl impl);
//使用用戶指定的 SocketImpl 建立一個未鏈接 Socket
ServerSocket(
int
port);
//建立綁定到特定端口的服務器套接字
ServerSocket(
int
port,
int
backlog);
//利用指定的 backlog 建立服務器套接字並將其綁定到指定的本地端口號
ServerSocket(
int
port,
int
backlog, InetAddress bindAddr);
//使用指定的端口、偵聽 backlog 和要綁定到的本地 IP地址建立服務器
|
構造方法的參數中,address、host和port分別是雙向鏈接中另外一方的IP地址、主機名和端 口號,stream指明socket是流socket仍是數據報socket,localPort表示本地主機的端口號,localAddr和bindAddr是本地機器的地址(ServerSocket的主機地址),impl是socket的父類,既能夠用來建立serverSocket又能夠用來建立Socket。count則表示服務端所能支持的最大鏈接數。
注意:必須當心選擇端口號。每個端口提供一種特定的服務,只有給出正確的端口,才 能得到相應的服務。0~1023的端口號爲系統所保留,例如http服務的端口號爲80,telnet服務的端口號爲21,ftp服務的端口號爲23, 因此咱們在選擇端口號時,最好選擇一個大於1023的數以防止發生衝突。
幾個重要的Socke方法:
1
2
3
|
public
InputStream getInputStream();
//方法得到網絡鏈接輸入,同時返回一個IutputStream對象實例
public
OutputStream getOutputStream();
//方法鏈接的另外一端將獲得輸入,同時返回一個OutputStream對象實例
public
Socket accept();
//用於產生"阻塞",直到接受到一個鏈接,而且返回一個客戶端的Socket對象實例。
|
"阻塞"是一個術語,它使程序運行暫時"停留"在這個地方,直到一個會話產生,而後程序繼續;一般"阻塞"是由循環產生的。
注意:其中getInputStream和getOutputStream方法均會產生一個IOException,它必須被捕獲,由於它們返回的流對象,一般都會被另外一個流對象使用。
四、基本的Client/Server程序
如下是一個基本的客戶端/服務器端程序代碼。主要實現了服務器端一直監聽某個端口,等待客戶端鏈接請求。客戶端根據IP地址和端口號鏈接服務器端,從鍵盤上輸入一行信息,發送到服務器端,而後接收服務器端返回的信息,最後結束會話。這個程序一次只能接受一個客戶鏈接。
ps:這個小例子寫好後,服務端一直接收不到消息,調試了好長時間,才發現誤使用了PrintWriter的print()方法,而BufferedReader的readLine()方法一直沒有遇到換行,因此一直等待讀取。我暈死~~@_@
客戶端程序:
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
|
package
com.socket;
import
java.io.BufferedReader;
import
java.io.InputStreamReader;
import
java.io.PrintWriter;
import
java.net.Socket;
public
class
SocketClient {
public
static
void
main(String[] args) {
try
{
/** 建立Socket*/
// 建立一個流套接字並將其鏈接到指定 IP 地址的指定端口號(本處是本機)
Socket socket =
new
Socket(
"127.0.0.1"
,
2013
);
// 60s超時
socket.setSoTimeout(
60000
);
/** 發送客戶端準備傳輸的信息 */
// 由Socket對象獲得輸出流,並構造PrintWriter對象
PrintWriter printWriter =
new
PrintWriter(socket.getOutputStream(),
true
);
// 將輸入讀入的字符串輸出到Server
BufferedReader sysBuff =
new
BufferedReader(
new
InputStreamReader(System.in));
printWriter.println(sysBuff.readLine());
// 刷新輸出流,使Server立刻收到該字符串
printWriter.flush();
/** 用於獲取服務端傳輸來的信息 */
// 由Socket對象獲得輸入流,並構造相應的BufferedReader對象
BufferedReader bufferedReader =
new
BufferedReader(
new
InputStreamReader(socket.getInputStream()));
// 輸入讀入一字符串
String result = bufferedReader.readLine();
System.out.println(
"Server say : "
+ result);
/** 關閉Socket*/
printWriter.close();
bufferedReader.close();
socket.close();
}
catch
(Exception e) {
System.out.println(
"Exception:"
+ e);
}
}
}
|
服務器端程序:
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
|
package
com.socket;
import
java.io.BufferedReader;
import
java.io.InputStreamReader;
import
java.io.PrintWriter;
import
java.net.ServerSocket;
import
java.net.Socket;
public
class
SocketServer {
public
static
void
main(String[] args) {
try
{
/** 建立ServerSocket*/
// 建立一個ServerSocket在端口2013監聽客戶請求
ServerSocket serverSocket =
new
ServerSocket(
2013
);
while
(
true
) {
// 偵聽並接受到此Socket的鏈接,請求到來則產生一個Socket對象,並繼續執行
Socket socket = serverSocket.accept();
/** 獲取客戶端傳來的信息 */
// 由Socket對象獲得輸入流,並構造相應的BufferedReader對象
BufferedReader bufferedReader =
new
BufferedReader(
new
InputStreamReader(socket.getInputStream()));
// 獲取從客戶端讀入的字符串
String result = bufferedReader.readLine();
System.out.println(
"Client say : "
+ result);
/** 發送服務端準備傳輸的 */
// 由Socket對象獲得輸出流,並構造PrintWriter對象
PrintWriter printWriter =
new
PrintWriter(socket.getOutputStream());
printWriter.print(
"hello Client, I am Server!"
);
printWriter.flush();
/** 關閉Socket*/
printWriter.close();
bufferedReader.close();
socket.close();
}
}
catch
(Exception e) {
System.out.println(
"Exception:"
+ e);
}
finally
{
// serverSocket.close();
}
}
}
|
五、多客戶端鏈接服務器
上面的服務器端程序一次只能鏈接一個客戶端,這在實際應用中顯然是不可能的。一般的網絡環境是多個客戶端鏈接到某個主機進行通信,因此咱們要對上面的程序進行改造。
設計思路:服務器端主程序監聽某一個端口,客戶端發起鏈接請求,服務器端主程序接收請求,同時構造一個線程類,用於接管會話。當一個Socket會話產生後,這個會話就會交給線程進行處理,主程序繼續進行監聽。
下面的實現程序流程是:客戶端和服務器創建鏈接,客戶端發送消息,服務端根據消息進行處理並返回消息,若客戶端申請關閉,則服務器關閉此鏈接,雙方通信結束。
客戶端程序:
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
|
package
com.socket;
import
java.io.BufferedReader;
import
java.io.InputStreamReader;
import
java.io.PrintWriter;
import
java.net.Socket;
public
class
SocketClient {
public
static
void
main(String[] args) {
try
{
Socket socket =
new
Socket(
"127.0.0.1"
,
2013
);
socket.setSoTimeout(
60000
);
PrintWriter printWriter =
new
PrintWriter(socket.getOutputStream(),
true
);
BufferedReader bufferedReader =
new
BufferedReader(
new
InputStreamReader(socket.getInputStream()));
String result =
""
;
while
(result.indexOf(
"bye"
) == -
1
){
BufferedReader sysBuff =
new
BufferedReader(
new
InputStreamReader(System.in));
printWriter.println(sysBuff.readLine());
printWriter.flush();
result = bufferedReader.readLine();
System.out.println(
"Server say : "
+ result);
}
printWriter.close();
bufferedReader.close();
socket.close();
}
catch
(Exception e) {
System.out.println(
"Exception:"
+ e);
}
}
}
|
服務器端程序:
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
|
package
com.socket;
import
java.io.*;
import
java.net.*;
public
class
Server
extends
ServerSocket {
private
static
final
int
SERVER_PORT =
2013
;
public
Server()
throws
IOException {
super
(SERVER_PORT);
try
{
while
(
true
) {
Socket socket = accept();
new
CreateServerThread(socket);
//當有請求時,啓一個線程處理
}
}
catch
(IOException e) {
}
finally
{
close();
}
}
//線程類
class
CreateServerThread
extends
Thread {
private
Socket client;
private
BufferedReader bufferedReader;
private
PrintWriter printWriter;
public
CreateServerThread(Socket s)
throws
IOException {
client = s;
bufferedReader =
new
BufferedReader(
new
InputStreamReader(client.getInputStream()));
printWriter =
new
PrintWriter(client.getOutputStream(),
true
);
System.out.println(
"Client("
+ getName() +
") come in..."
);
start();
}
public
void
run() {
try
{
String line = bufferedReader.readLine();
while
(!line.equals(
"bye"
)) {
printWriter.println(
"continue, Client("
+ getName() +
")!"
);
line = bufferedReader.readLine();
System.out.println(
"Client("
+ getName() +
") say: "
+ line);
}
printWriter.println(
"bye, Client("
+ getName() +
")!"
);
System.out.println(
"Client("
+ getName() +
") exit!"
);
printWriter.close();
bufferedReader.close();
client.close();
}
catch
(IOException e) {
}
}
}
public
static
void
main(String[] args)
throws
IOException {
new
Server();
}
}
|
六、信息共享
以上雖然實現了多個客戶端和服務器鏈接,可是仍然是消息在一個客戶端和服務器之間相互傳播。如今咱們要實現信息共享,即客戶端也能夠向其餘客戶端發送消息,服務器接收客戶端的消息而後廣播給其餘客戶端。相似於聊天室的那種功能,實現信息能在多個客戶端之間共享。
設計思路:客戶端循環能夠不停輸入向服務器發送消息,而且啓一個線程,專門用來監聽服務器端發來的消息並打印輸出。服務器端啓動時,啓動一個監聽什麼時候須要向客戶端發送消息的線程。每次接受客戶端鏈接請求,都啓一個線程進行處理,而且將客戶端信息存放到公共集合中。當客戶端發送消息時,服務器端將消息順序存入隊列中,當須要輸出時,從隊列中取出廣播到各客戶端處。
啓動服務端和客戶端程序(能夠啓多個客戶端),客戶端第一次輸入任意內容回車與服務器創建鏈接,而後按提示輸入登陸用戶名,接着就能夠以輸入的用戶名身份進行聊天。客戶端輸入showuser命令能夠查看在線用戶列表,輸入bye向服務器端申請退出鏈接。
PS:如下代碼在測試時發現了一箇中文亂碼小問題,當文件設置UTF-8編碼時,不管怎樣在代碼中設置輸入流編碼都不起做用,輸入中文仍然會亂碼。把文件設置爲GBK編碼後,不用在代碼中設置輸入流編碼都能正常顯示傳輸中文。
客戶端代碼:
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
|
import
java.io.BufferedReader;
import
java.io.InputStreamReader;
import
java.io.PrintWriter;
import
java.net.Socket;
public
class
SocketClient
extends
Socket{
private
static
final
String SERVER_IP =
"127.0.0.1"
;
private
static
final
int
SERVER_PORT =
2013
;
private
Socket client;
private
PrintWriter out;
private
BufferedReader in;
/**
* 與服務器鏈接,並輸入發送消息
*/
public
SocketClient()
throws
Exception{
super
(SERVER_IP, SERVER_PORT);
client =
this
;
out =
new
PrintWriter(
this
.getOutputStream(),
true
);
in =
new
BufferedReader(
new
InputStreamReader(
this
.getInputStream()));
new
readLineThread();
while
(
true
){
in =
new
BufferedReader(
new
InputStreamReader(System.in));
String input = in.readLine();
out.println(input);
}
}
/**
* 用於監聽服務器端向客戶端發送消息線程類
*/
class
readLineThread
extends
Thread{
private
BufferedReader buff;
public
readLineThread(){
try
{
buff =
new
BufferedReader(
new
InputStreamReader(client.getInputStream()));
start();
}
catch
(Exception e) {
}
}
@Override
public
void
run() {
try
{
while
(
true
){
String result = buff.readLine();
if
(
"byeClient"
.equals(result)){
//客戶端申請退出,服務端返回確認退出
break
;
}
else
{
//輸出服務端發送消息
System.out.println(result);
}
}
in.close();
out.close();
client.close();
}
catch
(Exception e) {
}
}
}
public
static
void
main(String[] args) {
try
{
new
SocketClient();
//啓動客戶端
}
catch
(Exception e) {
}
}
}
|
服務器端代碼:
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
|
import
java.io.BufferedReader;
import
java.io.IOException;
import
java.io.InputStreamReader;
import
java.io.PrintWriter;
import
java.net.ServerSocket;
import
java.net.Socket;
import
java.util.ArrayList;
import
java.util.LinkedList;
import
java.util.List;
public
class
Server
extends
ServerSocket {
private
static
final
int
SERVER_PORT =
2013
;
private
static
boolean
isPrint =
false
;
// 是否輸出消息標誌
private
static
List user_list =
new
ArrayList();
// 登陸用戶集合
private
static
List<ServerThread> thread_list =
new
ArrayList<ServerThread>();
// 服務器已啓用線程集合
private
static
LinkedList message_list =
new
LinkedList();
// 存放消息隊列
/**
* 建立服務端Socket,建立向客戶端發送消息線程,監聽客戶端請求並處理
*/
public
Server()
throws
IOException {
super
(SERVER_PORT);
// 建立ServerSocket
new
PrintOutThread();
// 建立向客戶端發送消息線程
try
{
while
(
true
) {
// 監聽客戶端請求,啓個線程處理
Socket socket = accept();
new
ServerThread(socket);
}
}
catch
(Exception e) {
}
finally
{
close();
}
}
/**
* 監聽是否有輸出消息請求線程類,向客戶端發送消息
*/
class
PrintOutThread
extends
Thread {
public
PrintOutThread() {
start();
}
@Override
public
void
run() {
while
(
true
) {
if
(isPrint) {
// 將緩存在隊列中的消息按順序發送到各客戶端,並從隊列中清除。
String message = (String) message_list.getFirst();
for
(ServerThread thread : thread_list) {
thread.sendMessage(message);
}
message_list.removeFirst();
isPrint = message_list.size() >
0
?
true
:
false
;
}
}
}
}
/**
* 服務器線程類
*/
class
ServerThread
extends
Thread {
private
Socket client;
private
PrintWriter out;
private
BufferedReader in;
private
String name;
public
ServerThread(Socket s)
throws
IOException {
client = s;
out =
new
PrintWriter(client.getOutputStream(),
true
);
in =
new
BufferedReader(
new
InputStreamReader(client.getInputStream()));
in.readLine();
out.println(
"成功連上聊天室,請輸入你的名字:"
);
|