在日常开发中采用Http协议进行数据传输的情况非常多,但这都是APP主动请求服务端,将数据传到服务器或者从服务器下载数据;那么如果我们想服务器主动联系我们或者我们频繁的上报消息给服务器,怎么弄呢,显然这时候用Http就不是一个好的方案了,而直接使用Socket是一个好的方法。
平时碰到的网络协议很多,很容易跟socket搞混,怎么区分呢?看下方:
1. IP :是网络层协议
2. TCP/UDP :传输层协议
3. HTTP : 应用层协议
通常所说的TCP/IP代表传输控制协议,而Socket是TCP/IP 网络的抽象接口
TCP和UDP使用IP协议从一个网络传输数据到另一个网络,把IP想象成一条高速公路,TCP和UDP想象成一辆辆卡车,高数公路允许其它卡车行驶,也就是IP协议允许其它协议行驶并找到其它电脑出口,而TCP和UDP这样的卡车装的货物就是像HTTP、FTP这样的应用层协议(UDP和TCP是FTP,HTTP所使用的传输层协议)。
虽然TCP和UDP都是使用IP协议来传输其它应用层协议的,但它们之间有显著不同,TCP提供有保障的数据传输,也就是有一个稳定的机制确保数据从一个端口传到另一个端口,比如打电话,必须对面接电话,你们之间的通话才会传输;而UDP不提供,就像发短信,不需要看对方,单方面直接发就行了。
HTTP是利用TCP在两台机器间(通常是web服务器和客户端)之间传输的协议
这些协议进行传输的前提是需要用IP协议来连接网络,比如货物没有卡车运输不行,卡车没有路跑不起来
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,是一组接口,不是协议;socket把复杂的TCP/IP协议隐藏在接口后面,对开发者来说,一组接口就是全部,用Socket来组装传输数据。
如下图
Tranfer Control protocol 的简称,中文是传输控制协议,是一种面向连接的可靠传输的协议。通过TCP协议传输,得到的是一个顺序的无差错的数据流。发送方和接收方的成对的两个socket之间必须建立连接,以便在TCp协议的基础上进行通信,当一个socket(通常都是server socket)等待建立连接时,另一个socket可以要求进行连接,一旦这两个socket连接起来,它们就可以进行双向数据传输,双方都可以进行发送或接收操作。
开发流程一般是
服务端:构建一个ServerSocket实例,指定一个本地端口;调用ServerSocket的accept()方法获取一个Socket(客户端)实例,这样就建立了与客户端的连接;通过这个Socket获取inputStream和outputStream,来进行写数据和读数据;最后调用Socket的close方法关闭与客户端的连接。
客户端:构建Socket实例,通过指定远程服务器地址和端口来建立与服务器的连接;通过这个Socket获取inputStream和outputStream,来进行写数据和读数据;最后调用Socket的close方法关闭与客户端的连接。
● ServerSocket( )
使用该构造方法在创建ServerSocket对象时并没有绑定端口号,这样的对象创建的服务器端没有监听任何端口,不能直接使用,还需要继续调用bind(SocketAdress endpoint)方法将其绑定到指定的端口号上才可以使用。
这个默认构造方法的用途是,允许服务器在绑定到特定端口之前,先设置ServerSocket的一些选项。因为一旦服务器与特定端口绑定,有些选项就不能再改变了。
如下代码
ServerSocket serverSocket=new ServerSocket();
serverSocket.setReuseAddress(true); //设置ServerSocket的选项
serverSocket.bind(new InetSocketAddress(8000)); //与8000端口绑定
ServerSocket serverSocket=new ServerSocket(8000);
serverSocket.setReuseAddress(true); //设置ServerSocket的选项
在第二种例子里,serverSocket.setReuseAddress(true)方法就不起任何作用了,因为SO_ REUSEADDR选项必须在服务器绑定端口之前设置才有效
● ServerSocket(intport )
使用该构造方法在创建ServerSocket对象时,就可以将其绑定到一个指定的端口上。端口号可以指定为0,此时系统就会分配给一个还没有被其他网络程序所使用的端口号。由于客户端需要根据指定的端口号来访问服务器端程序,因此端口号随机分配的情况并不常用,通常会让服务器端程序监听一个指定的端口号。
● ServerSocket(intport ,int backlog )
该构造方法就是在第二个构造方法基础上,增加了一个bscklog参数。管理客户连接请求的任务是由操作系统来完成的。操作系统把这些连接请求存储在一个先进先出的队列中。许多操作系统限定了队列的最大长度,一般为50。当队列中的连接请求达到了队列的最大容量时,服务器进程所在的主机会拒绝新的连接请求。只有当服务器进程通过ServerSocket的accept()方法从队列中取出连接请求,使队列腾出空位时,队列才能继续加入新的连接请求。
对于客户进程,如果它发出的连接请求被加入到服务器的队列中,就意味着客户与服务器的连接建立成功,客户进程从Socket构造方法中正常返回。如果客户进程发出的连接请求被服务器拒绝,Socket构造方法就会抛出ConnectionException。
ServerSocket构造方法的backlog参数用来显式设置连接请求队列的长度,它将覆盖操作系统限定的队列的最大长度。值得注意的是,在以下几种情况中,仍然会采用操作系统限定的队列的最大长度:
● ServerSocket(intport ,int backlog,InetAdress bindAddr )
该构造方法就是在第三种构造方法的基础上,还制定了相关的IP地址,这种情况适用于计算机上有多块网卡和多个IP的情况,我们可以明确规定ServerSocket在那块网卡或者IP地址上等待客户的连接请求,显然对于只有一块网卡的情况我们就不用专门指定了。
● isClosed()
判断ServerSocket是否关闭,只有执行了ServerSocket的close()方法,isClosed()方法才返回true;否则,即使ServerSocket还没有和特定端口绑定,isClosed()方法也会返回false。
● isBound()
判断ServerSocket是否已经与一个端口绑定,只要ServerSocket已经与一个端口绑定,即使它已经被关闭,isBound()方法也会返回true。
● setSoTimeout(int timeout)
设置获取客户端连接的超时时间,默认是0,表示永远不会超时;如果accept()方法阻塞时间超过了这个设置的时间,就会抛出SocketTimeoutException异常
● setResuseAddress(boolean on)
设置是否允许重用端口,这个默认值其实与操作系统有关系,有的操作系统是不允许重用端口的;当一个ServerSocket关闭后,这个端口可能不会立即被释放,确保接受了网络数据,然后再释放;如果在这个延迟的时间内,另一个ServerSocket启用也绑定了这个端口,如果设置false,就会绑定失败,抛出BindException异常;如果设置true,那这个端口就可以正常绑定。
另外需要注意的是setResuseAddress(true)方法必须在ServerSocket还没有绑定到一个本地端口之前调用,否则无效。此外,两个共用同一个端口的进程必须都调用serverSocket.setResuseAddress(true)方法,才能使得一个进程关闭ServerSocket后,另一个进程的ServerSocket还能够立刻重用相同端口。
● setReceiveBufferSize(int size)
设置服务器端的用于接收数据的缓冲区的大小,以字节为单位。如果要设置大于64K的缓冲区,则必须在ServerSocket绑定到特定端口之前进行设置才有效
ServerSocket serverSocket=new ServerSocket();
int size=serverSocket.getReceiveBufferSize();
if(size<131072) serverSocket.setReceiveBufferSize(131072); //把缓冲区的大小设为128K
serverSocket.bind(new InetSocketAddress(8000)); //与8000端口绑定
● Socket( )
使用该构造方法在创建Socket对象时,并没有指定IP地址和端口号,也就意味着只创建了客户端对象,并没与去连接任何服务器。
通过该构造方法创建对象后,还需要调用connect(SocketAdress endpoint)方法,才能完成与指定服务器端的连接,其中参数endpoint用于封装IP地址和端口号。
● Socket(Stringhost, int port )
使用该构造方法在创建Socket对象时,会根据参数去连接在指定地址和端口上运行的服务器程序,其中参数host接收的是一个字符串类型的IP地址。
● Socket(InetAdressaddress ,int port )
该方法在使用上与第二种构造方法类似,参数address用于接收一个InetAddress类型的对象,该对象用于封装一个IP地址。
● getInetAddress().getHostAddress()
获得远程server的IP 地址.
● getPort()
获得远程server的port.
● getLocalAddress().getHostAddress()
获得客户本地的IP 地址.
● getLocalPort()
获得客户本地的port.
● getInputStream()
获得输入流. 假设Socket 还没有连接, 或者已经关闭, 或者已经通过 shutdownInput() 方法关闭输入流, 那么此方法会抛出IOException.
● getOutputStream()
获得输出流, 假设Socket 还没有连接, 或者已经关闭, 或者已经通过 shutdownOutput() 方法关闭输出流, 那么此方法会抛出IOException.
● setTcpNoDelay(boolean on)
默认情况下, 发送数据採用Negale 算法. Negale 算法是指发送方发送的数据不会马上发出, 而是先放在缓冲区, 等缓存区满了再发出. 发送完一批数据后, 会等接收方对这批数据的回应, 然后再发送下一批数据. Negale 算法适用于发送方须要发送大批量数据, 而且接收方会及时作出回应的场合, 这样的算法通过降低数据传输的次数来提高通信效率.
假设发送方持续地发送小批量的数据, 而且接收方不一定会马上发送响应数据, 那么Negale 算法会使发送方执行非常慢. 对于GUI 程序, 如网络游戏程序(server须要实时跟踪client鼠标的移动), 这个问题尤其突出. client鼠标位置修改的信息须要实时发送到server上, 因为Negale 算法有缓冲, 大大减低了实时响应速度, 导致客户程序执行非常慢.
TCP_NODELAY 的默认值为 false, 表示采用 Negale 算法. 假设调用setTcpNoDelay(true)方法, 就会关闭 Socket的缓冲, 确保数据及时发送:
if(!socket.getTcpNoDelay()) socket.setTcpNoDelay(true);
假设Socket 的底层实现不支持TCP_NODELAY 选项, 那么getTcpNoDelay() 和 setTcpNoDelay 方法会抛出 SocketException.
● setResuseAddress(boolean on)
当接收方通过Socket 的close() 方法关闭Socket 时, 假设网络上还有发送到这个Socket 的数据, 那么底层的Socket 不会马上释放本地port, 而是会等待一段时间, 确保接收到了网络上发送过来的延迟数据, 然后再释放port. Socket接收到延迟数据后, 不会对这些数据作什么处理. Socket 接收延迟数据的目的是, 确保这些数据不会被其它碰巧绑定到相同port的新进程接收到.
当server程序关闭后, 有可能它的port还会被占用一段时间, 假设此时立马在同一个主机上重新启动server程序, 因为port已经被占用, 使得server程序无法绑定到该port, 启动失败.为了确保一个进程关闭Socket 后, 即使它还没释放port, 同一个主机上的其它进程还能够马上重用该port, 能够调用Socket 的setResuseAddress(true) 方法:
值得注意的是 socket.setResuseAddress(true) 方法必须在 Socket 还没有绑定到一个本地port之前调用, 否则运行 socket.setResuseAddress(true) 方法无效. 因此必须依照下面方式创建Socket 对象, 然后再连接远程server:
Socket socket = new Socket(); //此时Socke 对象为绑定本地port, 而且未连接远程server
socket.setReuseAddress(true);
SocketAddress localAddr = new InetSocketAddress("localhost",9000);
SocketAddress remoteAddr = new InetSocketAddress("localhost",8000);
socket.bind(localAddr); //与本地port绑定
socket.connect(remoteAddr); //连接远程server
此外, 两个共用同一个port的进程必须都调用 socket.setResuseAddress(true) 方法, 才可使得一个进程关闭 Socket后, 还有一个进程的 Socket 可以马上重用同样port.
● setSoTimeout(int milliseconds)
设定输入流读数据的等待超时时间, 单位为毫秒, 它的默认值为 0, 表示会无限等待, 永远不会超时.
● setSoLinger(boolean on, int seconds)
设置 Socket 关闭时的行为. 默认情况下, 运行 Socket 的 close() 方法, 该方法会马上返回, 但底层的 Socket 实际上并不马上关闭, 它会延迟一段时间, 直到发送全然部剩余的数据, 才会真正关闭 Socket, 断开连接.
假设运行下面方法:
socket.setSoLinger(true, 0);
那么运行Socket 的close() 方法, 该方法也会马上返回, 而且底层的 Socket 也会马上关闭, 全部未发送完的剩余数据被丢弃.
假设运行下面方法
socket.setSoLinger(true, 3600);
那么运行Socket 的 close() 方法, 该方法不会马上返回, 而是进入堵塞状态. 同一时候, 底层的 Socket 会尝试发送剩余的数据. 仅仅有满足下面两个条件之中的一个, close() 方法才返回:
⑴ 底层的 Socket 已经发送全然部的剩余数据;
⑵ 虽然底层的 Socket 还没有发送全然部的剩余数据, 但已经堵塞了 3600 秒(注意这里是秒, 而非毫秒), close() 方法的堵塞时间超过 3600 秒, 也会返回, 剩余未发送的数据被丢弃.
值得注意的是, 在以上两种情况内, 当close() 方法返回后, 底层的 Socket 会被关闭, 断开连接. 此外, setSoLinger(boolean on, int seconds) 方法中的 seconds 參数以秒为单位, 而不是以毫秒为单位.
假设未设置 SO_LINGER 选项, getSoLinger() 返回的结果是 -1, 假设设置了 socket.setSoLinger(true, 80) , getSoLinger() 返回的结果是 80.
当程序通过输出流写数据时, 只表示程序向网络提交了一批数据, 由网络负责输送到接收方. 当程序关闭 Socket, 有可能这批数据还在网络上传输, 还未到达接收方. 这里所说的 "未发送完的数据" 就是指这样的还在网络上传输, 未被接收方接收的数据.
● setReceiveBufferSize(int size)
设置 Socket 的用于输入数据的缓冲区的大小. 一般说来, 传输大的连续的数据块(基于HTTP 或 FTP 协议的通信) 能够使用较大的缓冲区, 这能够降低数据传输的次数, 提高数据传输的效率. 而对于交互频繁且单次传送数据量比較小的通信方式(Telnet 和 网络游戏), 则应该採用小的缓冲区, 确保小批量的数据能及时发送给对方. 这样的设定缓冲区大小的原则也相同适用于 Socket 的 SO_SNDBUF 选项.
假设底层 Socket 不支持 SO_RCVBUF 选项, 那么 setReceiveBufferSize() 方法会抛出 SocketException.
● setSendBufferSize(int size)
设置 Socket 的用于输出数据的缓冲区的大小. 假设底层 Socket 不支持 SO_SNDBUF 选项, setSendBufferSize() 方法会抛出 SocketException.
● setKeepAlive(boolean on)
设置为 true 时, 表示底层的TCP 实现会监视该连接是否有效. 当连接处于空暇状态(连接的两端没有互相传送数据) 超过了 2 小时时, 本地的TCP 实现会发送一个数据包给远程的 Socket. 假设远程Socket 没有发回响应, TCP实现就会持续尝试 11 分钟, 直到接收到响应为止. 假设在 12 分钟内未收到响应, TCP 实现就会自己主动关闭本地Socket, 断开连接. 在不同的网络平台上, TCP实现尝试与远程Socket 对话的时限有所区别.
默认值为 false, 表示TCP 不会监视连接是否有效, 不活动的client可能会永远存在下去, 而不会注意到server已经崩溃.
● setOOBInline(boolean on)
当 设置为true 时, 表示支持发送一个字节的 TCP 紧急数据. Socket 类的 sendUrgentData(int data) 方法用于发送一个字节的 TCP紧急数据.
默认值为 false, 在这样的情况下, 当接收方收到紧急数据时不作什么处理, 直接将其丢弃. 假设用户希望发送紧急数据, 应该把 设为 true,此时收件人将接收紧急数据
具体看代码
public class SocketServer {
public static void main(String[] args) {
new Thread(new RecevieMsgTask()).start();
new Thread(new SendMsgTask()).start();
}
static class RecevieMsgTask implements Runnable {
ServerSocket sServer = null;
Socket sClient = null;
BufferedReader br = null;
BufferedWriter bw = null;;
@Override
public void run() {
//每次重新获取前释放掉资源
try {
if(br != null)
br.close();
br = null;
if(bw != null){
bw.flush();
bw.close();
bw = null;
}
if(sClient != null)
sClient.close();
sClient = null;
} catch (IOException e) {
sClient = null;
br = null;
bw = null;
e.printStackTrace();
}
try {
if(sServer == null){
sServer = new ServerSocket(1935);
}
System.out.println("启动服务器....");
while (true) {
//获取连接过来的客户端 会一直堵塞,直到获取成功才往下走
sClient = sServer.accept();
System.out.println("APP客户端:"+sClient.getInetAddress().getHostAddress()+"已连接到服务器");
//如果是多个客户端,就把下面这段代码放到一个线程里执行
br = new BufferedReader(
new InputStreamReader(sClient.getInputStream(),"UTF-8"));
bw = new BufferedWriter(
new OutputStreamWriter(sClient.getOutputStream(),"UTF-8"));
while (true) {
//读取客户端发送来的消息 会一直堵塞 直到读取成功才往下走
String mess = br.readLine();
if(mess == null) return;
System.out.println("收到手机消息:"+mess);
//收到手机消息后就返回一些信息
bw.write("success"+"\n");
bw.flush();
}
}
} catch (IOException e) {
run();
e.printStackTrace();
}
}
}
static class SendMsgTask implements Runnable{
Socket sClient = null;
BufferedWriter br = null;
@Override
public void run() {
try {
connect();
for(int i=0; ;i++){
System.out.println("发消息给手机");
br.write("服务端主动发第"+i+"消息给客户端\n");
br.flush();
Thread.sleep(5000);
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
try {
Thread.sleep(1500);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
run();//如果APP的ServerSocket断了,那就尝试重新连接
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void connect() throws InterruptedException{
try {
if(br != null)
br.close();
br = null;
if(sClient != null)
sClient.close();
sClient = null;
} catch (IOException e) {
br = null;
sClient = null;
e.printStackTrace();
}
System.out.println("connect APPserver");
try {
sClient = new Socket("10.47.105.233", 5556);
br = new BufferedWriter(new OutputStreamWriter(sClient.getOutputStream(), "UTF-8"));
} catch (IOException e) {
Thread.sleep(1500);
connect();//连接App的ServerSocket失败就重新连接
e.printStackTrace();
}
}
}
第一个线程是启用一个ServerSocket,也就是服务端,然后不断的获取连接过来的客户端,一直去读客户端发过来的消息;
第二个线程是启用一个Socket,也就是客户端,去连接APP上的ServerSocket,不断的往APP发消息
public class MainActivity extends Activity implements OnClickListener {
private String TAG = "MainActivity";
private boolean isStop;
private TextView tv_receive;
private EditText et_send;
private Button connect;
private Button send;
private Button start;
private Button stop;
private String ip = "10.47.102.18";
private int port = 1935;
private Socket sClient;
private BufferedWriter bwClient;
private BufferedReader br;
private User user;
private Thread mServer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv_receive = (TextView) findViewById(R.id.tv_receive);
et_send = (EditText) findViewById(R.id.et_send);
connect = (Button) findViewById(R.id.connect);
send = (Button) findViewById(R.id.send);
start = (Button) findViewById(R.id.start);
stop = (Button) findViewById(R.id.stop);
connect.setOnClickListener(this);
send.setOnClickListener(this);
start.setOnClickListener(this);
stop.setOnClickListener(this);
user = new User();
}
@Override
public void onClick(View v) {
int id = v.getId();
switch (id) {
case R.id.connect:
Log.e(TAG, "connect");
if(sClient == null){
new ConnectTask().execute();
}
break;
case R.id.send:
Log.e(TAG, "send");
String content = et_send.getText().toString().trim();
if(TextUtils.isEmpty(content)){
Toast.makeText(this, "请输入内容", 0).show();
return;
}
user.setSendMsg(content+"\n");
break;
case R.id.start:
Log.e(TAG, "start");
isStop = false;
mServer = null;
mServer = new Thread(new GetClientTask());
mServer.start();
break;
case R.id.stop :
Log.e(TAG, "stop");
destorySocket();
break;
}
}
class ConnectTask extends AsyncTask{
@Override
protected String doInBackground(Void... params) {
String result = "0";
try {
sClient = new Socket(ip, port);
sClient.setSoTimeout(2000);
} catch (IOException e) {
Log.e(TAG, "e="+e.getMessage());
result = "1";
e.printStackTrace();
}
return result;
}
@Override
protected void onPostExecute(String result) {
super.onPostExecute(result);
if(TextUtils.equals(result, "0")){
new Thread(new SendMsg()).start();
Toast.makeText(MainActivity.this, "连接服务器成功", 0).show();
}else{
sClient = null;
Toast.makeText(MainActivity.this, "连接服务器失败", 0).show();
}
}
}
class SendMsg implements Runnable{
private InputStream is;
private OutputStream os;
@Override
public void run() {
try {
if(sClient != null){
is = sClient.getInputStream();
os = sClient.getOutputStream();
bwClient = new BufferedWriter(new OutputStreamWriter(os,"UTF-8"));
br = new BufferedReader(new InputStreamReader(is,"UTF-8"));
}
while (true) {
if(!TextUtils.isEmpty(user.getSendMsg())){
bwClient.write(user.getSendMsg());
bwClient.flush();
user.setSendMsg("");
handler.sendEmptyMessage(1);
String result = br.readLine();//返回值
Log.e(TAG, "服务器返回 result="+result);
}
Thread.sleep(500);
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Handler handler = new Handler(){
public void handleMessage(android.os.Message msg) {
if(msg.what == 0){
if(!TextUtils.isEmpty(user.getReceiceMsg())){
tv_receive.append(user.getReceiceMsg()+"\n");
user.setReceiceMsg("");
}
}else{
Toast.makeText(getApplicationContext(), "发送成功", 0).show();
}
};
};
ServerSocket sServer = null;
Socket sClientServer = null;
BufferedReader brServer = null;
class GetClientTask implements Runnable{
@Override
public void run() {
if(isStop) return;
try {
if(brServer != null) brServer.close();
brServer = null;
} catch (Exception e) {
brServer = null;
}
try {
if(sClientServer != null) sClientServer.close();
sClientServer = null;
} catch (Exception e) {
sClientServer = null;
}
try {
Log.e(TAG,"正在启动客户端服务器....");
if(sServer == null){
sServer = new ServerSocket(5556);
}
sClientServer = sServer.accept();
Log.e(TAG,"服务端:"+sClientServer.getInetAddress().
getHostAddress()+"已连接到客户端");
brServer = new BufferedReader(new InputStreamReader(
sClientServer.getInputStream(), "UTF-8"));
while (true) {
if(isStop) return;
user.setReceiceMsg(brServer.readLine());
handler.sendEmptyMessage(0);
Log.e(TAG, "收到服务器消息");
}
} catch (IOException e) {
Log.e(TAG, "GetClientTask e="+e.getMessage());
try {
Thread.sleep(1500);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
run();
e.printStackTrace();
}
}
}
@Override
public void onBackPressed() {
try {
if(bwClient != null)
bwClient.flush();
bwClient.close();
bwClient = null;
} catch (Exception e) {
bwClient = null;
}
try {
if(sClient != null)
sClient.shutdownOutput();
sClient.close();
sClient = null;
} catch (Exception e) {
sClient = null;
}
destorySocket();
super.onBackPressed();
}
private void destorySocket(){
isStop = true;
try {
if(brServer != null) brServer.close();
brServer = null;
} catch (Exception e) {
brServer = null;
}
try {
if(sClientServer != null) sClientServer.close();
sClientServer = null;
} catch (Exception e) {
sClientServer = null;
}
try {
if(sServer != null) sServer.close();
sServer = null;
} catch (IOException e) {
sServer = null;
}
if(mServer != null){
mServer.interrupt();//在run没有停止时调用.interrupt()没有效果。
mServer = null;
}
}
}
大致流程就是在异步任务里构建Socket,连接服务器,然后不停的读取用户输入信息,发送到服务端;
然后构建一个ServerSocket不停的读取服务端发送过来的消息
TCP通信就这样了
User Datagram protocol 的简称,是一种无连接的协议,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能否到达目的地,到达包的顺序,到达目的地的时间以及内容的正确性都是不能被保证的。
有如下特点
开发流程其实客户端和服务端操作基本类似,不像TCP有服务端客户端很明显的区分
开发流程一般是
try {
byte[] buff = new byte[4];
DatagramSocket ds = new DatagramSocket(1935);
DatagramPacket dp = new DatagramPacket(buff, buff.length);
while (true) {
ds.receive(dp);//此方法会一直阻塞,直到获取到数据
System.out.println(new String(buff));
}
} catch (IOException e) {
System.out.println("e "+e.getMessage());
e.printStackTrace();
}
try {
DatagramSocket socket = new DatagramSocket();
socket.connect(new InetSocketAddress("10.47.102.18", 1935));
while (true) {
byte[] buff = "在吗".getBytes();
System.out.println("length="+buff.length);
DatagramPacket dp = new DatagramPacket(buff, buff.length);
socket.send(dp);
Thread.sleep(2000);
}
} catch (IOException e) {
System.out.println("e="+e.getMessage());
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
比较麻烦的一点是缓冲区大小的选择。接收数据报时,如果缓冲区不够大,放不下的数据部分会丢失。UDP数据报的数据部分最大为65507(IP最大数据长度65535-IP首部20-UDP首部8),但底层UDP实现设置的最大数据长度可能更小,如8192字节。所以UDP包的大小尽量不要太大。
如果希望复用DatagramPacket对象以避免重复创建多个相似对象的开销,可以使用DatagramPacket的set方法:
public synchronized void setAddress(InetAddress iaddr)
public synchronized void setData(byte[] buf)
public synchronized void setData(byte[] buf, int offset, int length)
public synchronized void setLength(int length)
public synchronized void setPort(int iport)
public synchronized void setSocketAddress(SocketAddress address)
这些方法中最实用的可能是setData(byte[] buf, int offset, int length)方法。通过改变offset的值,可以轻易地实现将一个大数组分成多个数据报发送。
另外,每当调用DatagramSocket的receive()方法获得一个数据报时,DatagramPacket的length属性会被设置成数据报的数据部分的长度,这会导致后面的数据包的大小无法超过前面的数据包的大小。因此,如果要复用DatagramPacket对象,必须在每次提取数据完毕后调用setLength(buf.length)重置length属性值。
最近接到一个任务,原本实际功能是有个红外探测设备检测到事件或者告警后需要将数据上报到服务器,但是为了方便测试数据上报功能,就想开发一个模拟器模拟设备数据上报功能,于是就有了今天这篇文章;可能没沟通清楚,其实是有Rest接口的,可是我最终使用的是Socket接口去开发的, 这里用的不是IO里面的ServerSocket和Socket,而是NIO里面的ServerSocketChannel和SocketChannel,平时使用的不是很频繁,故记录下其使用
需求其实挺简单的,就是模拟设备按照文档向服务器发送数据,也不需要解析服务器返回的数据,就是不停的发送就行了
先来定义服务端
public class NIOServer implements Runnable{
//通道管理器
private Selector selector;
//读数据缓存区
private ByteBuffer inBuffer;
//写数据缓存区
private ByteBuffer outBuffer;
public NIOServer() {
inBuffer = ByteBuffer.allocate(16);
outBuffer = ByteBuffer.allocate(24);
}
@Override
public void run() {
startListening();
}
public void startListening(){
try {
//创建ServerSocket通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
/**
* 将通道设置为非阻塞式,这样它的accept方法将会立即返回
* 如果没有新进来的连接将会返回null
* 注意判断空
*/
serverSocketChannel.configureBlocking(false);
//获取该通道对应的ServerSocket
ServerSocket serverSocket = serverSocketChannel.socket();
//绑定端口
serverSocket.bind(new InetSocketAddress(2898));
//创建通道管理器
selector = Selector.open();
/**
* 将通道注册到通道管理器,并为该通道注册OP_ACCEPT事件,
* 这样有客户端的连接事件来了后selector.select()会返回
* 否则会一直阻塞
*/
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
//当事件来了后该方法会返回,否则一直阻塞
selector.select();
//获取监听事件
Set selectionKeys = selector.selectedKeys();
Iterator iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
//获取事件
SelectionKey next = iterator.next();
//删除事件,避免重复处理
iterator.remove();
if (next.isAcceptable()) {
//说明是客户端连接事件,接收客户端连接
handleAccept(next);
} else if (next.isReadable()){
//这是收到客户端消息
handleRead(next);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void handleAccept(SelectionKey next) {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) next.channel();
try {
//获取客户端Socket通道,这里是非阻塞式
SocketChannel accept = serverSocketChannel.accept();
accept.configureBlocking(false);
//可以给客户端回复连接成功消息
outBuffer.clear();
outBuffer.putInt(10);
outBuffer.putInt(0);
outBuffer.flip();
accept.write(outBuffer);
/**
* 将该通道注册到通道管理器,并注册OP_READ事件
* 后续如果客户端有向缓存中write数据,下次轮询时,则isReadable()=true
*/
accept.register(selector,SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
private void handleRead(SelectionKey next) {
//获取其监听的Channel通道
SocketChannel socketChannel = (SocketChannel) next.channel();
try {
inBuffer.clear();
//将通道数据读到缓存区 客户端发送过来的数据
int count = socketChannel.read(inBuffer);
if (count > 0) {
inBuffer.flip();
int msg = inBuffer.getInt();
int index = inBuffer.getInt();
int status = inBuffer.getInt();
System.out.print("服务端:收到客户端数据 :index=" + index +",status="+status+ " \n");
}
outBuffer.clear();
//往通道写数据,给客户端回复消息
outBuffer.putInt(201);
outBuffer.putInt(0);
outBuffer.flip();
socketChannel.write(outBuffer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
注释已经很清楚了,服务端做的事主要就是接受客户端连接,然后获取消息后返回消息,没有主动给客户端发送消息;当然,如果需要主动发消息就得将serverSocketChannel.accept()获取到的客户端保存起来,通过它们发送消息
再定义客户端
public class NIOClient implements Runnable {
//通道管理器
private Selector selector;
//Socket通道
private SocketChannel socketChannel;
//上报标志
private boolean isStop;
//接收数据缓存区
private ByteBuffer inBuffer;
//发送数据缓存区
private ByteBuffer outBuffer;
//待上报数据队列
private ArrayDeque acuDataDeque = new ArrayDeque<>(64);
//当队列为空时,暂停上报线程,让出CPU资源
private Object lock = new Object();
private String SERVER_IP = "10.47.102.28";
private int SERVER_PORT = 2898;
public NIOClient() {
inBuffer = ByteBuffer.allocate(16);
outBuffer = ByteBuffer.allocate(24);
}
public void stop(){
isStop = true;
}
public void addAcuData(AcuData acuData){
synchronized (lock) {
setStatus(acuData.getStatus());
setIndex(acuData.getIndex());
acuDataDeque.offer(acuData);
lock.notifyAll();
}
}
public void setStatus(int status){
if (status > 2 || status<0) {
throw new IllegalParameterException("The rang of status value is [0,2],the current value of status is " + status);
}
}
public void setIndex(int index){
if (index > 257 || index<1) {
throw new IllegalParameterException("The rang of index value is [1,257],the current value of index is " + index);
}
}
@Override
public void run() {
startPush();
}
private void startPush(){
try {
//创建通道管理器
selector = Selector.open();
/**
* 创建Socket通道
* 一个客户端(Socket)复用这一个通道,即后续所有I/O操作共用这一个通道
* 正常使用时切勿关闭
* 可在与客户端通信发生异常或者正常断开连接时关闭该通道,释放Socket连接
*/
socketChannel = SocketChannel.open();
//设置为非阻塞式
socketChannel.configureBlocking(false);
/**
* 客户端连接服务器,但是这里并没有真正连接
* 在后续的channel.finishConnect()才能完成连接
*/
socketChannel.connect(new InetSocketAddress(SERVER_IP,SERVER_PORT));
/**
* 将该通道注册到通道管理器,并注册OP_CONNECT事件,意思该通道对连接事件感兴趣
* 这样isConnectable = true
* OP_ACCEPT : 一个Server Socket Channel准备好接受一个新进入的客户端连接,称为 接受连接就绪事件
* OP_CONNECT : 一个Socket Channel成功连接到一个服务器,称为 连接就绪事件
* OP_WRITE : 一个等待写数据的通道,可以执行写操作了, 称为写就绪事件
* OP_READ : 一个有数据可读的通道,可以执行读操作了,称为读就绪事件
*/
socketChannel.register(selector, SelectionKey.OP_CONNECT);
while (!isStop) {
/**
* 获取一组可以进行I/O操作的事件,保存在selector中
* 如果服务端没接收客户端的连接,这里获取连接就绪事件将会阻塞
* 除第一次连接就绪事件外,当服务端发消息过来,该方法会返回,否则一直阻塞
*/
selector.select();
//获取所有选择器
Set selectionKeys = selector.selectedKeys();
Iterator iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
//迭代处理选择器
SelectionKey next = iterator.next();
//删除该选择器,防止重复处理
iterator.remove();
if (next.isConnectable()) {//是连接就绪事件
// System.out.print("客户端:连接就绪事件"+"\n");
handlerConnect(next);
} else if (next.isReadable()) {//是读就绪事件
// System.out.print("客户端:读就绪事件"+"\n");
handlerRead(next);
}
}
}
System.out.print("客户端:退出"+"\n");
socketChannel.close();
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 连接服务器
* @param next
*/
private void handlerConnect(SelectionKey next) {
/**
* 获取与客户端通信的Socket通道
*/
SocketChannel socketChannel = (SocketChannel) next.channel();
try {
//判断此通道上的连接操作是否正在进行
if (socketChannel.isConnectionPending()) {
//完成连接操作
socketChannel.finishConnect();
}
//设置非阻塞
socketChannel.configureBlocking(false);
/**
* 给该通道注册OP_READ事件
* 这样该通道就对读就绪事件感兴趣
* 比如服务器发送消息过来,那么下次轮询时isReadable = true
* 后续就可以使用这个通道与服务器通信
*/
socketChannel.register(selector, SelectionKey.OP_READ);
//连接成功后开辟线程发送数据
new Thread(new SendData()).start();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 读取服务器数据
* @param next
*/
private void handlerRead(SelectionKey next) {
/**
* 获取与客户端通信的Socket通道
*/
SocketChannel socketChannel = (SocketChannel) next.channel();
try {
inBuffer.clear();
int count = socketChannel.read(inBuffer);
if (count > 0) {
inBuffer.flip();
int code = inBuffer.getInt();
int ret = inBuffer.getInt();
}
//这里也可以往服务器发送数据,只不过是被动的,必须要接收到服务器数据,客户端才能发送数据
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 主动往服务器发送数据
*/
class SendData implements Runnable{
@Override
public void run() {
while (!isStop) {
synchronized (lock) {
if (acuDataDeque.size() == 0) {
try {
System.out.print("暂停上报线程"+"\n");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
isStop = true;
}
}
}
try {
if (buildData()) {
socketChannel.write(outBuffer);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private boolean buildData(){
AcuData acuData = acuDataDeque.poll();
if (acuData == null) return false;
outBuffer.clear();
outBuffer.putInt(201);
outBuffer.putInt(acuData.getIndex());
outBuffer.putInt(acuData.getStatus());
outBuffer.putInt(65535);
outBuffer.putInt(250);
outBuffer.putInt(260);
outBuffer.flip();
return true;
}
}
客户端为了更好的控制数据上报,就将发送功能拉出来,没有放在handlerRead方法里做了