摘 要:TCP/IP通訊協議是可靠的面向鏈接的網絡協議,它在通訊兩端各創建一個Socket,從而在兩端造成網絡虛擬鏈路,進而應用程序可經過能夠經過虛擬鏈路進行通訊。Java對於基於TCP協議的網絡通訊提供了良好的封裝,使用Socket對象表明兩端的通訊接口,經過Socket產生I/O流進行網絡通訊。java
自建ServerSocket服務端時可能因PC與手機平板終端未接入同一路由器,所以沒法訪問服本地IP,能夠嘗試如下兩種方式解決android
關鍵詞: Socket; ServerSocket;本地IP; addresswindows
UnknownHostException:主機名或IP錯誤服務器
ConnectException:服務器拒絕鏈接、服務器沒有啓動、超出隊列數網絡
SocketTimeoutException:鏈接超時多線程
BindException:Socket對象沒法與制定的本地IP地址或端口綁定app
ServerSocket()throws IOExceptionsocket
ServerSocket(int port)throws IOExceptionide
ServerSocket(int port, int backlog)throws IOException函數
ServerSocket(int port, int backlog, InetAddress bindAddr)throws IOException
1) port服務端要監聽的端口;backlog客戶端鏈接請求的隊列長度;bindAddr服務端綁定IP
2) 若是端口被佔用或者沒有權限使用某些端口會拋出BindException錯誤。譬如1~1023的端口須要管理員才擁有權限綁定。
3)若是設置端口爲0,則系統會自動爲其分配一個端口;
4) bindAddr用於綁定服務器IP,爲何會有這樣的設置呢,譬若有些機器有多個網卡。
5) ServerSocket一旦綁定了監聽端口,就沒法更改。ServerSocket()能夠實如今綁定端口前設置其餘的參數。
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
public class SimpleServer {
public static void main (String [] args) throws IOException
{
ServerSocket ss= new ServerSocket(30000,10,InetAddress.getByName ("172.18.85.60"));
System.out.println(ss.getInetAddress());
while (true){
Socket s = ss.accept();
OutputStream os = s.getOutputStream();
os.write("您好,你已成功鏈接了服務器!\n".getBytes("utf-8"));
os.close();
s.close();
}
}
}
Socket()
Socket(InetAddress address, int port)throws UnknownHostException, IOException
Socket(InetAddress address, int port, InetAddress localAddress, int localPort)throws IOException
Socket(String host, int port)throws UnknownHostException, IOException
Socket(String host, int port, InetAddress localAddress, int localPort)throws IOException
除去第一種不帶參數的以外,其它構造函數會嘗試創建與服務器的鏈接。若是失敗會拋出IOException錯誤。若是成功,則返回Socket對象。
InetAddress是一個用於記錄主機的類,其靜態getHostByName(String msg)能夠返回一個實例,其靜態方法getLocalHost()也能夠得到當前主機的IP地址,並返回一個實例。Socket(String host, int port, InetAddress localAddress, int localPort)構造函數的參數分別爲目標IP、目標端口、綁定本地IP、綁定本地端口。
Address:此處IP爲開啓的虛擬WiFi IP,以下圖(2)中的192.168.191.1,由於此方法可保證PC與手機終端接入同一路由器(若是手機終端開的虛擬機則本機IP:172.20.9.4(圖3)與虛擬WiFi IP:192.168.191.1均可);
getInetAddress(); 遠程服務端的IP地址
getPort(); 遠程服務端的端口
getLocalAddress() 本地客戶端的IP地址
getLocalPort() 本地客戶端的端口
getInputStream(); 得到輸入流
getOutStream(); 得到輸出流
值得注意的是,在這些方法裏面,最重要的就是getInputStream()和getOutputStream()了。
isClosed(); //鏈接是否已關閉,若關閉,返回true;不然返回false
isConnect(); //若是曾經鏈接過,返回true;不然返回false
isBound(); //若是Socket已經與本地一個端口綁定,返回true;不然返回false
若是要確認Socket的狀態是否處於鏈接中,下面語句是很好的判斷方式。
public class SimpleClient extends Activity
{
private final static String HOST = "localhost";
EditText show;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
show = (EditText) findViewById(R.id.show);
new Thread()
{
@Override
public void run()
{
show.setText("lianjie");
try
{
// 創建鏈接到遠程服務器的Socket
Socket socket = new Socket("172.18.85.60", 30000); //①
//socket.setSoTimeout(10000);
Log.i("本機地址",socket.getLocalAddress().toString());
String string = socket.getRemoteSocketAddress().toString();
System.out.println(string);
// 將Socket對應的輸入流包裝成BufferedReader
try{
BufferedReader br = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
// 進行普通IO操做
String line = br.readLine();
show.setText("來自服務器的數據:" + line);
// 關閉輸入流、socket
br.close();
socket.close();
}
catch(SocketTimeoutException ex)
{
System.out.println("TimeOut!!!");
}
}
catch (IOException e)
{
e.printStackTrace();
}
}
}.start();
}
}
1)Server端:
Class 1:
public class MyServer
{
// 定義保存全部Socket的ArrayList
public static ArrayList<Socket> socketList
= new ArrayList<Socket>();
public static void main(String[] args)
throws IOException
{
ServerSocket ss = new ServerSocket(30000);
while(true)
{
// 此行代碼會阻塞,將一直等待別人的鏈接
Socket s = ss.accept();
socketList.add(s);
// 每當客戶端鏈接後啓動一條ServerThread線程爲該客戶端服務
new Thread(new ServerThread(s)).start();
}
}
}
Class 2:
public class ServerThread implements Runnable
{
// 定義當前線程所處理的Socket
Socket s = null;
// 該線程所處理的Socket所對應的輸入流
BufferedReader br = null;
public ServerThread(Socket s)
throws IOException
{
this.s = s;
// 初始化該Socket對應的輸入流
br = new BufferedReader(new InputStreamReader(
s.getInputStream() , "utf-8")); //②
}
public void run()
{
try
{
String content = null;
// 採用循環不斷從Socket中讀取客戶端發送過來的數據
while ((content = readFromClient()) != null)
{
// 遍歷socketList中的每一個Socket,
// 將讀到的內容向每一個Socket發送一次
for (Socket s : MyServer.socketList)
{
OutputStream os = s.getOutputStream();
os.write((content + "\n").getBytes("utf-8"));
}
}
}
catch (IOException e)
{
e.printStackTrace();
}
}
// 定義讀取客戶端數據的方法
private String readFromClient()
{
try
{
return br.readLine();
}
// 若是捕捉到異常,代表該Socket對應的客戶端已經關閉
catch (IOException e)
{
// 刪除該Socket。
MyServer.socketList.remove(s); //①
}
return null;
}
}
2)Socket客戶端:
Class 1:
public class MultiThreadClient extends Activity
{
// 定義界面上的兩個文本框
EditText input;
TextView show;
// 定義界面上的一個按鈕
Button send;
Handler handler;
// 定義與服務器通訊的子線程
ClientThread clientThread;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
input = (EditText) findViewById(R.id.input);
send = (Button) findViewById(R.id.send);
show = (TextView) findViewById(R.id.show);
handler = new Handler() //①
{
@Override
public void handleMessage(Message msg)
{
// 若是消息來自於子線程
if (msg.what == 0x123)
{
// 將讀取的內容追加顯示在文本框中
show.append("\n" + msg.obj.toString());
}
}
};
clientThread = new ClientThread(handler);
// 客戶端啓動ClientThread線程建立網絡鏈接、讀取來自服務器的數據
new Thread(clientThread).start(); //①
send.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View v)
{
try
{
// 當用戶按下發送按鈕後,將用戶輸入的數據封裝成Message,
// 而後發送給子線程的Handler
Message msg = new Message();
msg.what = 0x345;
msg.obj = input.getText().toString();
clientThread.revHandler.sendMessage(msg);
// 清空input文本框
input.setText("");
}
catch (Exception e)
{
e.printStackTrace();
}
}
});
}
}
Class 2:
public class ClientThread implements Runnable
{
private Socket s;
// 定義向UI線程發送消息的Handler對象
private Handler handler;
// 定義接收UI線程的消息的Handler對象
public Handler revHandler;
// 該線程所處理的Socket所對應的輸入流
BufferedReader br = null;
OutputStream os = null;
public ClientThread(Handler handler)
{
this.handler = handler;
}
public void run()
{
try
{
s = new Socket("192.168.191.1", 30000); //此處IP若爲開啓的虛擬WiFi IP,以下圖(2)中的//192.168.191.1,由於此方法可保證PC與手機終端接入同一路由器(若是手機終端開的虛擬機則本機IP與虛//擬WiFi IP均可);
br = new BufferedReader(new InputStreamReader(
s.getInputStream()));
os = s.getOutputStream();
// 啓動一條子線程來讀取服務器響應的數據
new Thread()
{
@Override
public void run()
{
String content = null;
// 不斷讀取Socket輸入流中的內容。
try
{
while ((content = br.readLine()) != null)
{
// 每當讀到來自服務器的數據以後,發送消息通知程序界面顯示該數據
Message msg = new Message();
msg.what = 0x123;
msg.obj = content;
handler.sendMessage(msg);
}
}
catch (IOException e)
{
e.printStackTrace();
}
}
}.start();
// 爲當前線程初始化Looper
Looper.prepare();
// 建立revHandler對象
revHandler = new Handler()
{
@Override
public void handleMessage(Message msg)
{
// 接收到UI線程中用戶輸入的數據
if (msg.what == 0x345)
{
// 將用戶在文本框內輸入的內容寫入網絡
try
{
os.write((msg.obj.toString() + "\r\n")
.getBytes("utf-8"));
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
};
// 啓動Looper
Looper.loop();
}
catch (SocketTimeoutException e1)
{
System.out.println("網絡鏈接超時!!");
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
查看本機IP:win + R -> cmd ->ipconfig
IPv4爲你的IP (圖 1)
解決方法:
1)windows搭建無線局域網,手機接入局域網,new Socket(局域網IP,端口號)
(1)以管理員身份運行命令提示符:
快捷鍵win+R→輸入cmd→回車
(2)啓用並設定虛擬WiFi網卡:
運行命令:netsh wlan set hostednetwork mode=allow ssid=laozhang key=12345678
此命令有三個參數,mode:是否啓用虛擬WiFi網卡,改成disallow則爲禁用。
ssid:無線網名稱,最好用英文(以wuminPC爲例)。
key:無線網密碼,八個以上字符(以wuminWiFi爲例)。
以上三個參數能夠單獨使用,例如只使用mode=disallow能夠直接禁用虛擬Wifi網卡。
(3)開啓成功後,網絡鏈接中會多出一個網卡爲「Microsoft Virtual WiFi Miniport Adapter」的無線鏈接2,爲方便起見,將其重命名爲虛擬WiFi。若沒有,只需更新無線網卡驅動就OK了。
(4)設置Internet鏈接共享:
在「網絡鏈接」窗口中,右鍵單擊已鏈接到Internet的網絡鏈接,選擇「屬性」→「共享」,勾上「容許其餘······鏈接(N)」並選擇「虛擬WiFi」。
(5)開啓無線網絡:
繼續在命令提示符中運行:netsh wlan start hostednetwork
netsh wlan stop hostednetwork
(將start改成stop便可關閉該無線網,之後開機後要啓用該無線網只需再次運行此命令便可)查看網卡是否支持虛擬WIFI:netsh wlan show drivers)
(圖 2) PC鏈接無線網
(圖 3) PC鏈接有線網
2)使用獵豹WiFi快速共享網絡,完成鏈接
6.3 怎麼知道本身的電腦IP是內網仍是外網:能夠ping本機IP(cmd+ipconfig),若是IP是10.x.x.x;172.x.x.x; 192.168.x.x。 基本斷定爲內網,其它形式通常爲外網。
6.4
6.4.1 如何訪問內網的服務器:若是是經過路由上網,外部訪問這個主機時,經過外網訪問的只是路由的外網 IP,路由器裏面須要進行路由端口映射(將主機的IP和端口填入路由的端口映射表中),這個主機才能被訪問到。校園網也相似,可是通常的用戶是沒有權限設置的。若是在校園網內網裏面作服務器,但願被外網訪問,需慎重。
6.4.2 須要注意的是:好比個人本機IP是100.82.121.40,這是一個外網IP,可是實際上我是不 能經過該IP地址訪問服務器的,由於這是移動網的IP,而移動網是經過NAT(網絡地址轉換)形式的,因此在外網經過這個IP是不能本機的服務器的(可能須要相似於路由器的映射那樣的轉換才行)。而與此同時,通過測試,聯通和電信的網的IP是一個完整的外網IP(能夠經過該IP地址訪問服務器)---這是我的測試獲得的一些結論,可能會有錯誤。
6.4.3 區域網內測試:手機和電腦在同一個區域網內(經過wifi鏈接),這時候客戶端能夠經過鏈接電腦的內網 地址(區域網內的IP,通常是192.168.1.1相似的)鏈接PC服務器(端口任意),也能夠經過直接鏈接電腦的外網地址(普通的IP4地址,注意不 是內網的形式),其實本質只是經過內網鏈接(好比我外網是移動網,wifi模式下客戶端能夠經過外網IP鏈接誒服務器,可是斷開Wifi,就會找不到服務器,因此這其實只是一種假象,就像本機客戶端經過外網IP鏈接本機服務器同樣)
6.4.4 外網測試:手機經過外網IP訪問PC端的服務器,經測試,只能是聯通網和電信網才行,移動網是NAT形式不能訪問。
參考文獻:
[1] 瘋狂Android講義,李剛著,電子工業出版社
[2] cnblogs.com android問題分析