1-简介:
概念:在网络上的两个程序通过一个双向的通信连接实现数据的交换,这个双向链路的一端称为一个socket。
组成:由一个IP地址和一个端口号唯一确定,是TCP/IP 协议的一个十分流行的编程界面。
应用:socket编程比基于URL的HTTP网络编程提供了更高的传输效率、更强大的功能和更灵活的控制,但却复杂一些。
地位:socket已经是java中层次最低的网络编程接口,在java中直接操作协议中更低的层次,需要使用java本地方法调用(JNI)。
基础:Server端监听某个端口是否有连接请求,Client端向Server端发出连接请求,Server端向Client端发回Accept消息,一个连接就建立起来了;
Server和Client端都可以用Send和Write等方法,与对方通信,Java在包java.net中提供了两个类Socket和ServerSocket。
目前网上的资源大都无法实现点对点通信,要么用服务器广播,本实例将实现socket 指定的点点通信。
2-功能简介:
界面设计有点挫,重点在于功能,这是安卓客户端, 上面的 spinner下拉列表用来选择在线用户,
中间的TextView用于显示接收到的信息,(这里设计略简单,只做DEMO参考级别),
最下面输入聊天内容,发送按钮进行点对点通信。
3-服务器编程:
S1.新建一个java工程充当java 服务器,命名为ChatServer:
S2 编写服务器程序:
服务器有三个联网类组成:
S2.1.ServerThread
服务线程,启动服务器时启动,开启服务ServerSocket用以监听端口收到的客户端信息。
package com.rxz.web;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class ServerThread implements Runnable {
// ServerSocket 类对象
private ServerSocket serverSocket = null;
// 服务器端口设置
private static final int PORT = 8888;
// 存储接收到客户端的消息
public List<Message> messagelt = null;
// 服务器启动标志位
private boolean isStart = true;
// 存储客户端socket
public HashMap<String, ClientHandlerThread> clientsMap = null;
/**
* 构造函数
*/
public ServerThread(){
// 初始化 message 存储list
messagelt = new ArrayList<Message>();
// 初始化客户端socket
clientsMap = new HashMap<String, ClientHandlerThread>();
try {
// 开启服务器socket
serverSocket = new ServerSocket(PORT);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 启动消息处理线程
new Thread(new MessageHandlerThread(this)).start();
}
/**
* 一旦有客户端连入,就启动客户端处理线程,并加入服务器客户端HashMap中
*/
@Override
public void run() {
// TODO Auto-generated method stub
while(isStart){
try {
// 获取一个连接客户端
Socket socket = serverSocket.accept();
System.out.println("连入一个客户端:" + socket.getInetAddress().getHostAddress());
// 创建clientHandler线程
ClientHandlerThread clientRunnable = new ClientHandlerThread(socket, this);
Thread clientThread = new Thread(clientRunnable);
clientThread.start();
// 将成功获取到的客户端保存起来
if(socket != null){
synchronized(clientsMap){
clientsMap.put(socket.getInetAddress().getHostAddress(), clientRunnable);
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
/**
* 关闭 ServerSocket
*/
public void finalize(){
// 关闭 ServerSocket
try {
serverSocket.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
serverSocket = null;
}
}
class Message{
public String IP;
public String info;
public Message(String iP, String info) {
super();
IP = iP;
this.info = info;
}
}
S2.2.ClientHandlerThread
客户端处理线程,当有一个客户端连入服务器时,启动一个线程接收客户端信息。
package com.rxz.web;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
public class ClientHandlerThread implements Runnable {
// 客户端socket
public Socket clientSocket = null;
// socket 的输入,输出流
private DataInputStream in = null;
public DataOutputStream out = null;
// 服务器线程ServerThread
private ServerThread serverThread = null;
/**
* 构造函数
* @param socket
* @param serverThread
*/
public ClientHandlerThread(Socket socket, ServerThread serverThread){
this.serverThread = serverThread;
this.clientSocket = socket;
// 获取对服务器操作的输入输出流
try {
in = new DataInputStream(clientSocket.getInputStream());
out = new DataOutputStream(clientSocket.getOutputStream());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* 监听对应的客户端是否有消息发送
*/
@Override
public void run() {
// TODO Auto-generated method stub
while(true){
try {
String iP = in.readUTF();
String info = in.readUTF();
Message message = new Message(iP, info);
// 打印接收信息
System.out.println("[" + clientSocket.getInetAddress().getHostAddress() + "] -> [" + message.IP + "]:" + message.info);
// 将客户端发送的信息存储到 Message LIST中
synchronized (serverThread.messagelt) {
serverThread.messagelt.add(message);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
break;
}
}
}
}
S2.3.MessageHandlerThread
package com.rxz.web;
import java.io.IOException;
import java.util.Iterator;
public class MessageHandlerThread implements Runnable{
// 客户端线程
private ClientHandlerThread clientThread = null;
// 服务器线程
private ServerThread serverThread = null;
// 处理的消息
private Message message = null;
/**
* 构造函数
* @param serverThread
*/
public MessageHandlerThread(ServerThread serverThread){
this.serverThread = serverThread;
}
@Override
public void run() {
// TODO Auto-generated method stub
while(true){
// 线程休眠100ms
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 获取要处理的信息
synchronized (serverThread.messagelt) {
// 判断是否有未发送的信息
if(serverThread.messagelt.isEmpty()){
continue;
}
message = serverThread.messagelt.get(0);
}
// 处理信息
synchronized (serverThread.clientsMap) {
if(message.IP.equals("10.0.2.2")) message.IP = "127.0.0.1";
// 获取要发送的客户端
clientThread = serverThread.clientsMap.get(message.IP);
try {
// 获取所有用户
if(message.info.equals("{GETALL}")){
String info = "";
Iterator<String> iterator = serverThread.clientsMap.keySet().iterator();
while(iterator.hasNext()) {
info += serverThread.clientsMap.get(iterator.next()).clientSocket.getInetAddress().getHostAddress();
info += ";";
}
message.info = info;
}
// 发送数据
clientThread.out.writeUTF(message.info);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 从server LIST中删除掉已经处理的消息
serverThread.messagelt.remove(message);
}
}
}
}
4-安卓客户端编程
S1 Socket通信模块编程
该设计按照单例模式设计,即全局不构造对象,类变量只有一个实例。
这样的设计提高了socket利用率,节省了代码量。
package com.rxz.web;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
public class ChatClient {
private static Socket clientSocket = null;
private static DataInputStream in = null;
private static DataOutputStream out = null;
private static String IP = "10.0.2.2";
private static int PORT = 8888;
public static Socket getSocket(){
if(clientSocket == null){
try {
clientSocket = new Socket(IP, PORT);
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return clientSocket;
}
public static DataInputStream getDataInputStream(){
if(in == null){
getSocket();
try {
in = new DataInputStream(clientSocket.getInputStream());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return in;
}
public static DataOutputStream getDataOutputStream(){
if(out == null){
getSocket();
try {
out = new DataOutputStream(clientSocket.getOutputStream());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return out;
}
}
S2 安卓端界面简单设计
S2.1.MainActivity
Spinner控件的设计,用于选择在线用户;
TextView控件为了显示聊天记录信息;
EditView控件为了获取用户输入的数据信息;
Button控件为了发送用户信息;
package com.rxzchatdemo;
import java.io.IOException;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import com.rxz.web.ChatClient;
public class MainActivity extends Activity implements OnClickListener, Runnable {
private Spinner userList = null;
private Button sendBtn = null;
private EditText infoEdit = null;
private String message = null;
private String[] mUsers = null;
private TextView infoArea = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
userList = (Spinner) super.findViewById(R.id.userList);
infoEdit = (EditText) findViewById(R.id.infoEdit);
sendBtn = (Button) findViewById(R.id.sendBtn);
infoArea = (TextView) findViewById(R.id.chatArea);
sendBtn.setOnClickListener(this);
try {
//获得本机IP
InetAddress addr = InetAddress.getLocalHost();
String ip = addr.getHostAddress().toString();
ChatClient.getDataOutputStream().writeUTF(ip);
ChatClient.getDataOutputStream().writeUTF("{GETALL}");
String response = ChatClient.getDataInputStream().readUTF();
mUsers = response.split(";");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
android.R.layout.simple_spinner_item, mUsers);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
userList.setAdapter(adapter);
new Thread(this).start();
}
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
switch(v.getId()){
case R.id.sendBtn:
String ip = userList.getSelectedItem().toString();
String info = infoEdit.getText().toString();
try {
ChatClient.getDataOutputStream().writeUTF(ip);
ChatClient.getDataOutputStream().writeUTF(info);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
break;
}
}
@Override
public void run() {
// TODO Auto-generated method stub
while(true){
try {
message = ChatClient.getDataInputStream().readUTF();
message += "\n";
mHandler.sendMessage(mHandler.obtainMessage());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
private Handler mHandler = new Handler(){
/**
* 获取一个消息,刷新对话框
*/
public void handleMessage(Message msg){
infoArea.append(message);
super.handleMessage(msg);
}
};
}
S2.2.activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity" >
<Spinner
android:id="@+id/userList"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/chatArea"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1.05"/>
<EditText
android:id="@+id/infoEdit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10" >
<requestFocus />
</EditText>
<Button
android:id="@+id/sendBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="发送" />
</LinearLayout>
S3.增加联网权限
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
5-运行效果
先启动服务器,然后启动客户端,选择聊天对象,即可实现点对点通信。
PS.几点遗憾:
1.Spinner控件没有获取到点击事件,导致无法更新在线人信息,但这个与socket通信无关,后期将会把更新版本补充上。
2.由于开启虚拟机测试,不能实现现场点对点通信(虚拟机IP相同导致的),真机测试,效果会更好。
3.之前想用传统的ObjectInputStream,然后将信息交换定义为一系列的Message类,但考虑到服务器不知用于java通信,如果这样写可能会让跨语言通信失败。
本实例精华重在一个HashMap映射思想和,MessageHandlerThread消息处理线程的设计,用以实现的点点通信机制。
附上源码: