浅谈android Socket 通信及自建ServerSocket服务端常见问题

 

摘  要:TCP/IP通信协议是可靠的面向连接的网络协议,它在通信两端各建立一个Socket,从而在两端形成网络虚拟链路,进而应用程序可通过可以通过虚拟链路进行通信。Java对于基于TCP协议的网络通信提供了良好的封装,使用Socket对象代表两端的通信接口,通过Socket产生I/O流进行网络通信。

自建ServerSocket服务端时可能因PC与手机平板终端未接入同一路由器,因此无法访问服本地IP,可以尝试以下两种方式解决

关键词: Socket; ServerSocket;本地IP; address

1   SocketServerSocket的交互过程

 

2   Socket涉及到的异常类型

UnknownHostException:主机名或IP错误

ConnectException:服务器拒绝连接、服务器没有启动、超出队列数

SocketTimeoutException:连接超时

BindExceptionSocket对象无法与制定的本地IP地址或端口绑定

3   ServerSocket创建TCP服务器端

3.1   构造函数

ServerSocket()throws IOException

ServerSocket(int port)throws IOException

ServerSocket(int port, int backlog)throws IOException

ServerSocket(int port, int backlog, InetAddress bindAddr)throws IOException

3.2   注意

1) port服务端要监听的端口;backlog客户端连接请求的队列长度;bindAddr服务端绑定IP

2) 如果端口被占用或者没有权限使用某些端口会抛出BindException错误。譬如1~1023的端口需要管理员才拥有权限绑定。

3)如果设置端口为0,则系统会自动为其分配一个端口;

4) bindAddr用于绑定服务器IP,为什么会有这样的设置呢,譬如有些机器有多个网卡。

5) ServerSocket一旦绑定了监听端口,就无法更改。ServerSocket()可以实现在绑定端口前设置其他的参数。

3.3   示例

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();

   }

   }

}

 

4   Socket进行通信

4.1   构造函数

 

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与手机终端接入同一路由器(如果手机终端开的虚拟机则本机IP172.20.9.4(图3)与虚拟WiFi IP192.168.191.1都可);

 

4.2   Socket方法

 

getInetAddress();      远程服务端的IP地址

getPort();          远程服务端的端口

getLocalAddress()      本地客户端的IP地址

getLocalPort()        本地客户端的端口

getInputStream();     获得输入流

getOutStream();      获得输出流

值得注意的是,在这些方法里面,最重要的就是getInputStream()getOutputStream()了。

 

4.3    Socket状态

 

isClosed();            //连接是否已关闭,若关闭,返回true;否则返回false

isConnect();      //如果曾经连接过,返回true;否则返回false

isBound();            //如果Socket已经与本地一个端口绑定,返回true;否则返回false

如果要确认Socket的状态是否处于连接中,下面语句是很好的判断方式。

 

4.4    示例

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();

}

}

5   多线程示例

1Server端:

    Class 1

public class MyServer

{

// 定义保存所有SocketArrayList

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();

}

}

}

6   常见问题及解决方法:

6.1   本地IP设置错误

     查看本机IPwin + R -> cmd ->ipconfig

     技术分享

      

     IPv4为你的IP                      (图 1

 

6.2   本由于未手机终端与PC服务端未接入同一路由器

解决方法:

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即可关闭该无线网,以后开机后要启用该无线网只需再次运行此命令即可)查看网卡是否支持虚拟WIFInetsh wlan show drivers

                             (图 2)   PC连接无线网

             技术分享

                                  (图 3)   PC连接有线网

           技术分享

                                      

2)使用猎豹WiFi快速共享网络,完成连接

 

6.3      怎么知道自己的电脑IP是内网还是外网:可以ping本机IP(cmd+ipconfig),如果IP10.x.x.x172.x.x.x 192.168.x.x。 基本判定为内网,其它形式一般为外网。

6.4     

   6.4.1  如何访问内网的服务器:如果是通过路由上网,外部访问这个主机时,通过外网访问的只是路由的外网 IP,路由器里面需要进行路由端口映射(将主机的IP和端口填入路由的端口映射表中),这个主机才能被访问到。校园网也类似,但是一般的用户是没有权限设置的。如果在校园网内网里面做服务器,希望被外网访问,需慎重。

   6.4.2  需要注意的是:比如我的本机IP100.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问题分析

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。