JAVA网络编程

参考<<疯狂JAVA编程>>中的第17章 网络编程

 

InetAddress简介
InetAddress是Java对IP地址的封装,在java.net中有许多类都使用到了InetAddress,包括ServerSocket,Socket,DatagramSocket等等

import java.net.*;
/**
 * 演示InetAddress类的基本使用
 */
public class InetAddressDemo {
         public static void main(String[] args) {
                   try{
                            //使用域名创建对象
                            InetAddress inet1 = InetAddress.getByName("www.163.com");
                            System.out.println(inet1);//  www.163.com/220.181.28.50
                            //使用IP创建对象
                            InetAddress inet2 = InetAddress.getByName("127.0.0.1");
                            System.out.println(inet2);//  /127.0.0.1
                            //获得本机地址对象
                            InetAddress inet3 = InetAddress.getLocalHost();
                            System.out.println(inet3);// chen/192.168.1.100
                            //获得对象中存储的域名
                            String host = inet3.getHostName();
                            System.out.println("域名:" + host);// 域名:chen
                            //获得对象中存储的IP
                            String ip = inet3.getHostAddress();// IP:192.168.1.100
                            System.out.println("IP:" + ip);
                   }catch(Exception e){}
         }
}

 

URLDecoder和URLEncoder简介
URLDecoder和URLEncoder主要完成application/x-www-form-urlencoded字符串和普通字符串之间的相关转化

import java.net.*;

public class URLDecoderTest{
    public static void main(String[] args) throws Exception{
        //将application/x-www-form-urlencoded字符串
        //转换成普通字符串
        String keyWord = URLDecoder.decode("%E6%9D%8E%E5%88%9A+j2ee", "UTF-8");
        System.out.println(keyWord);//李刚 j2ee
        
        //将普通字符串转换成
        //application/x-www-form-urlencoded字符串
        String urlStr = URLEncoder.encode("ROR敏捷开发最佳指南" , "GBK");
        System.out.println(urlStr);//ROR%C3%F4%BD%DD%BF%AA%B7%A2%D7%EE%BC%D1%D6%B8%C4%CF
    }
}

 


URL和URLConnection介绍(实现多线程下载,以及断点下载)

URL的openConnection()方法返回一个URLConnection对象,该对象表示应用程序和URL之间的通信链接。程序可以通过URLConnection对象向该URL发送请求,读取URL引用的资源。

import java.io.*;
import java.net.*;

//定义下载从start到end的内容的线程
class DownThread extends Thread
{
    //定义字节数组(取水的竹筒)的长度
    private final int BUFF_LEN = 32;
    //定义下载的起始点
    private long start;
    //定义下载的结束点
    private long end;
    //下载资源对应的输入流
    private InputStream is;
    //将下载到的字节输出到raf中
    private RandomAccessFile raf ;

    //构造器,传入输入流,输出流和下载起始点、结束点
    public DownThread(long start , long end 
        , InputStream is , RandomAccessFile raf)
    {
        //输出该线程负责下载的字节位置
        System.out.println(start + "---->"  + end);
        this.start = start;
        this.end = end;
        this.is = is;
        this.raf = raf;
    }
    public void run()
    {
        try
        {
            is.skip(start);
            raf.seek(start); 
            //定义读取输入流内容的的缓存数组(竹筒)
            byte[] buff = new byte[BUFF_LEN];
            //本线程负责下载资源的大小
            long contentLen = end - start;
            //定义最多需要读取几次就可以完成本线程的下载
            long times = contentLen / BUFF_LEN + 4;
            //实际读取的字节数
            int hasRead = 0;
            for (int i = 0; i < times ; i++)
            {
                hasRead = is.read(buff);
                //如果读取的字节数小于0,则退出循环!
                if (hasRead < 0)
                {
                    break;
                }
                raf.write(buff , 0 , hasRead);
            }            
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
        //使用finally块来关闭当前线程的输入流、输出流
        finally
        {
            try
            {
                if (is != null)
                {
                    is.close();
                }
                if (raf != null)
                {
                    raf.close();
                }
            }
            catch (Exception ex)
            {
                ex.printStackTrace();
            }
        }
    }
}
public class MutilDown
{
    public static void main(String[] args)
    {
        final int DOWN_THREAD_NUM = 4;
        final String OUT_FILE_NAME = "down.jpg";
        InputStream[] isArr = new InputStream[DOWN_THREAD_NUM];
        RandomAccessFile[] outArr = new RandomAccessFile[DOWN_THREAD_NUM];
        try
        {
            //创建一个URL对象
            URL url = new URL("http://images.china-pub.com/"
                + "ebook35001-40000/35850/shupi.jpg");
            //以此URL对象打开第一个输入流
            isArr[0] = url.openStream();
            long fileLen = getFileLength(url);
            System.out.println("网络资源的大小" + fileLen);
            //以输出文件名创建第一个RandomAccessFile输出流
            outArr[0] = new RandomAccessFile(OUT_FILE_NAME , "rw");
            //创建一个与下载资源相同大小的空文件
            for (int i = 0 ; i < fileLen ; i++ )
            {
                outArr[0].write(0);
            }
            //每线程应该下载的字节数
            long numPerThred = fileLen / DOWN_THREAD_NUM;
            //整个下载资源整除后剩下的余数
            long left = fileLen % DOWN_THREAD_NUM;
            for (int i = 0 ; i < DOWN_THREAD_NUM; i++)
            {
                //为每个线程打开一个输入流、一个RandomAccessFile对象,
                //让每个线程分别负责下载资源的不同部分。
                if (i != 0)
                {
                    //以URL打开多个输入流
                    isArr[i] = url.openStream();
                    //以指定输出文件创建多个RandomAccessFile对象
                    outArr[i] = new RandomAccessFile(OUT_FILE_NAME , "rw");
                }
                //分别启动多个线程来下载网络资源
                if (i == DOWN_THREAD_NUM - 1 )
                {
                    //最后一个线程下载指定numPerThred+left个字节
                    new DownThread(i * numPerThred , (i + 1) * numPerThred + left
                        , isArr[i] , outArr[i]).start();
                }
                else
                {
                    //每个线程负责下载一定的numPerThred个字节
                    new DownThread(i * numPerThred , (i + 1) * numPerThred
                        , isArr[i] , outArr[i]).start();
                }
            }
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
    //定义获取指定网络资源的长度的方法
    public static long getFileLength(URL url) throws Exception
    {
        long length = 0;
        //打开该URL对应的URLConnection。
        URLConnection con = url.openConnection();
        //获取连接URL资源的长度
        long size = con.getContentLength();
        length = size;
        return length;
    }
}

 

关于Get和Post请求
只是发送Get方式请求,使用connect方法建立和远程资源之间的实际连接就OK了。
如果需要发送POST方式的请求,需要获取URLConnection实例对应的输出流来发送请求参数。

import java.io.*; 
import java.net.*;
import java.util.*;

public class TestGetPost{
    /**
     * 向指定URL发送GET方法的请求
     * @param url 发送请求的URL
     * @param param 请求参数,请求参数应该是name1=value1&name2=value2的形式。
     * @return URL所代表远程资源的响应
     */
    public static String sendGet(String url , String param) {
        String result = "";
        BufferedReader in = null;
        try
        {
            String urlName = url + "?" + param;
            URL realUrl = new URL(urlName);
            
            //打开和URL之间的连接
            URLConnection conn = realUrl.openConnection();
            
            //设置通用的请求属性
            conn.setRequestProperty("accept", "*/*"); 
            conn.setRequestProperty("connection", "Keep-Alive"); 
            conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"); 
                
            //建立实际的连接
            conn.connect(); 
            
            //获取所有响应头字段
            Map<String,List<String>> map = conn.getHeaderFields();
            
            //遍历所有的响应头字段
            for (String key : map.keySet()){
                System.out.println(key + "--->" + map.get(key));
            }
            
            //定义BufferedReader输入流来读取URL的响应
            in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            
            String line;
            while ((line = in.readLine())!= null){
                result += "\n" + line;
            }
        }
        catch(Exception e){
            System.out.println("发送GET请求出现异常!" + e);
            e.printStackTrace();
        }
        finally{//使用finally块来关闭输入流
            try{
                if (in != null){
                    in.close();
                }
            }
            catch (IOException ex){
                ex.printStackTrace();
            }
        }
        return result;
    }

    /**
     * 向指定URL发送POST方法的请求
     * @param url 发送请求的URL
     * @param param 请求参数,请求参数应该是name1=value1&name2=value2的形式。
     * @return URL所代表远程资源的响应
     */    
    public static String sendPost(String url,String param){
        PrintWriter out = null;
        BufferedReader in = null;
        String result = "";
        try{
            URL realUrl = new URL(url);
            //打开和URL之间的连接
            URLConnection conn = realUrl.openConnection();
            
            //设置通用的请求属性
            conn.setRequestProperty("accept", "*/*"); 
            conn.setRequestProperty("connection", "Keep-Alive"); 
            conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"); 
            
            //发送POST请求必须设置如下两行
            conn.setDoOutput(true);
            conn.setDoInput(true);
            
            //获取URLConnection对象对应的输出流
            out = new PrintWriter(conn.getOutputStream());
            //发送请求参数
            out.print(param);
            //flush输出流的缓冲
            out.flush();
            
            //定义BufferedReader输入流来读取URL的响应
            in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String line;
            while ((line = in.readLine())!= null){
                result += "\n" + line;
            }
        }
        catch(Exception e){
            System.out.println("发送POST请求出现异常!" + e);
            e.printStackTrace();
        }
        //使用finally块来关闭输出流、输入流
        finally{
            try{
                if (out != null){
                    out.close();
                }
                if (in != null){
                    in.close();
                }
            }
            catch (IOException ex){
                ex.printStackTrace();
            }
        }
        return result;
    }

    //提供主方法,测试发送GET请求和POST请求
    public static void main(String args[]){
        //发送GET请求
        String s = TestGetPost.sendGet("http://localhost:8888/abc/login.jsp",null);
        System.out.println(s);
        
        //发送POST请求
        String s1 = TestGetPost.sendPost("http://localhost:8888/abc/a.jsp","user=李刚&pass=abc");
        System.out.println(s1);
    }
}

 

TCP协议网络编程
(ServerSocket,Socket),要建立连接.
在Java语言中,对于TCP方式的网络编程提供了良好的支持,在实际实现时,以java.net.Socket类代表客户端连接,以java.net.ServerSocket类代表服务器端连接。
java主要关注的是在传输层 的应用,而对于底层的传输,可以不必关心它。而在传输层,TCP,UDP是两种传输数据流的方式。
由于TCP需要建立专用的虚拟连接以及确认传输是否正确,所以使用TCP方式的速度稍微慢一些,而且传输时产生的数据量要比UDP稍微大一些

例子程序(实现类似QQ的通讯程序,包括群聊和私聊)
服务端源码:

public interface YeekuProtocol{
    //定义协议字符串的长度
    int PROTOCOL_LEN = 2;
    
    //下面是一些协议字符串,服务器和客户端交换的信息
    //都应该在前、后添加这种特殊字符串。
    String MSG_ROUND = "§γ";
    String USER_ROUND = "∏∑";
    String LOGIN_SUCCESS = "1";
    String NAME_REP = "-1";
    String PRIVATE_ROUND = "★【";
    String SPLIT_SIGN = "※";
}

//扩展HashMap类,MyMap类要求value也不可重复
public class YeekuMap<K,V> extends HashMap<K,V>{
    //根据value来删除指定项
    public void removeByValue(Object value) {
        for (Object key : keySet()){
            if (get(key) == value){
                remove(key);
                break;
            }
        }
    }

    //获取所有value组成的Set集合
    public Set<V> valueSet() {
        Set<V> result = new HashSet<V>();
        //遍历所有key组成的集合
        for (K key : keySet()){
            //将每个key对应的value添加到result集合中
            result.add(get(key));
        }
        return result;
    }

    //根据value查找key。
    public K getKeyByValue(V val) {
        //遍历所有key组成的集合
        for (K key : keySet()){
            //如果指定key对应的value与被搜索的value相同
            //则返回对应的key
            if (get(key).equals(val) 
                && get(key) == val){
                return key;
            }
        }
        return null;
    }
    
    //重写HashMap的put方法,该方法不允许value重复
    public V put(K key,V value){
        //遍历所有value组成的集合
        for (V val : valueSet() ){
            //如果指定value与试图放入集合的value相同
            //则抛出一个RuntimeException异常
            if (val.equals(value) 
                && val.hashCode() == value.hashCode()){
                throw new RuntimeException
                    ("MyMap实例中不允许有重复value!"); 
            }
        }
        return super.put(key , value);
    }
}


public class Server {
    private static final int SERVER_PORT = 30000;
    
    //使用MyMap对象来保存每个客户名字和对应输出流之间的对应关系。
    public static YeekuMap<String , PrintStream> clients =new YeekuMap<String , PrintStream>();
    
    public void init(){
        ServerSocket ss = null;
        try{
            //建立监听的ServerSocket
            ss = new ServerSocket(SERVER_PORT);
            
            //采用死循环来不断接受来自客户端的请求
            while(true){
                Socket socket = ss.accept();//阻塞式
                new ServerThread(socket).start();
            }
        }
        catch (IOException ex){
            System.out.println("服务器启动失败,是否端口" + SERVER_PORT + "已被占用?");
        }
        finally{
            try{
                if (ss != null){
                    ss.close();
                }
            }
            catch (IOException ex){
                ex.printStackTrace();
            }
            System.exit(1);
        }
    }
    
    public static void main(String[] args){
        Server server = new Server();
        server.init();
    }
}


public class ServerThread extends Thread{
    private Socket socket;
    BufferedReader br = null;
    PrintStream ps = null;
    
    //定义一个构造器,用于接收一个Socket来创建ServerThread线程
    public ServerThread(Socket socket){
        this.socket = socket;
    }
    
    public void run(){
        try{
            //获取该Socket对应的输入流
            br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            //获取该Socket对应的输出流
            ps = new PrintStream(socket.getOutputStream());
            String line = null;
            
            while((line = br.readLine())!= null){
                //如果读到的行以MyProtocol.USER_ROUND开始,并以其结束,
                //可以确定读到的是用户登陆的用户名
                if (line.startsWith(YeekuProtocol.USER_ROUND)
                    && line.endsWith(YeekuProtocol.USER_ROUND)){
                    //得到真实消息
                    String userName = getRealMsg(line);
                    //如果用户名重复
                    if (Server.clients.containsKey(userName)){
                        System.out.println("重复");
                        ps.println(YeekuProtocol.NAME_REP);
                    }
                    else{
                        System.out.println("成功");
                        ps.println(YeekuProtocol.LOGIN_SUCCESS);
                        Server.clients.put(userName , ps);//登录成功,记录scoket对应的输出流
                    }
                }
                //如果读到的行以YeekuProtocol.PRIVATE_ROUND开始,并以其结束,
                //可以确定是私聊信息,私聊信息只向特定的输出流发送
                else if (line.startsWith(YeekuProtocol.PRIVATE_ROUND) 
                    && line.endsWith(YeekuProtocol.PRIVATE_ROUND)){
                    //得到真实消息
                    String userAndMsg = getRealMsg(line);
                    //以SPLIT_SIGN来分割字符串,前面部分是私聊用户,后面部分是聊天信息
                    String user = userAndMsg.split(YeekuProtocol.SPLIT_SIGN)[0];
                    String msg = userAndMsg.split(YeekuProtocol.SPLIT_SIGN)[1];
                    //获取私聊用户对应的输出流,并发送私聊信息
                    Server.clients.get(user).println(Server.clients.getKeyByValue(ps) + "悄悄地对你说:" + msg);
                }
                //公聊要向每个Socket发送
                else{
                    //得到真实消息
                    String msg = getRealMsg(line);
                    //遍历clients中的每个输出流
                    for (PrintStream clientPs : Server.clients.valueSet()){
                        clientPs.println(Server.clients.getKeyByValue(ps)+ "说:" + msg);
                    }
                }
            }
        }
        //捕捉到异常后,表明该Socket对应的客户端已经出现了问题
        //所以程序将其对应的输出流从Map中删除
        catch (IOException e){
            Server.clients.removeByValue(ps);
            System.out.println(Server.clients.size());
            //关闭网络、IO资源
            try{
                if (br != null){
                    br.close();
                }
                if (ps != null){
                    ps.close();
                }
                if (socket != null){
                    socket.close();    
                }
            }
            catch (IOException ex)
            {
                ex.printStackTrace();
            }
        }
    }
    
    //将读到的内容去掉前后的协议字符,恢复成真实数据
    public String getRealMsg(String line){
        return line.substring(YeekuProtocol.PROTOCOL_LEN, line.length() - YeekuProtocol.PROTOCOL_LEN);
    }
}

客户端源码:

public class Client{
    private static final int SERVER_PORT = 30000;

    private Socket socket;
    private PrintStream ps;
    private BufferedReader brServer;
    private    BufferedReader keyIn;
    
    public void init(){
        try{
            //初始化代表键盘的输入流
            keyIn = new BufferedReader(new InputStreamReader(System.in));
            
            //连接到服务器
            socket = new Socket("127.0.0.1", SERVER_PORT);
            
            //获取该Socket对应的输入流和输出流
            ps = new PrintStream(socket.getOutputStream());
            brServer = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String tip = "";
            
            //采用循环不断地弹出对话框要求输入用户名
            while(true){
                String userName = JOptionPane.showInputDialog(tip + "输入用户名");
                //将用户输入的用户名的前后增加协议字符串后发送
                ps.println(YeekuProtocol.USER_ROUND + userName+ YeekuProtocol.USER_ROUND);
                
                //读取服务器的响应
                String result = brServer.readLine();
                
                //如果用户重复,开始下次循环
                if (result.equals(YeekuProtocol.NAME_REP)){
                    tip = "用户名重复!请重新";
                    continue;
                }
                //如果服务器返回登陆成功,结束循环
                if (result.equals(YeekuProtocol.LOGIN_SUCCESS)){
                    break;
                }
            }
        }
        catch (UnknownHostException ex){
            System.out.println("找不到远程服务器,请确定服务器已经启动!");
            closeRs();
            System.exit(1);
        }
        catch (IOException ex){
            System.out.println("网络异常!请重新登陆!");
            closeRs();
            System.exit(1);
        }
        
        //以该Socket对应的输入流启动ClientThread线程
        new ClientThread(brServer).start();
    }
    
    //定义一个读取键盘输出,并向网络发送的方法
    private void readAndSend()
    {
        try{
            //不断读取键盘输入
            String line = null;
            while((line = keyIn.readLine()) != null){
                //如果发送的信息中有冒号,且以//开头,则认为想发送私聊信息
                if (line.indexOf(":") > 0 && line.startsWith("//")){
                    line = line.substring(2);
                    //冒号之前的是私聊用户,冒号之后的是聊天信息
                    ps.println(YeekuProtocol.PRIVATE_ROUND + 
                        line.split(":")[0] + YeekuProtocol.SPLIT_SIGN + 
                        line.split(":")[1] + YeekuProtocol.PRIVATE_ROUND);
                }
                else{
                    ps.println(YeekuProtocol.MSG_ROUND + line+ YeekuProtocol.MSG_ROUND);
                }
            }
        }
        catch (IOException ex){
            System.out.println("网络通信异常!请重新登陆!");
            closeRs();
            System.exit(1);
        }
    }

    //关闭Socket、输入流、输出流的方法
    private void closeRs(){
        try{
            if (keyIn != null){
                ps.close();
            }
            if (brServer != null){
                ps.close();
            }
            if (ps != null){
                ps.close();
            }
            if (socket != null){
                keyIn.close();
            }
        }
        catch (IOException ex){
            ex.printStackTrace();
        }
    }

    public static void main(String[] args){
        Client client = new Client();
        client.init();
        client.readAndSend();
    }
}


public class ClientThread extends Thread{
    //该客户端线程负责处理的输入流
    BufferedReader br = null;
    //使用一个网络输入流来创建客户端线程
    public ClientThread(BufferedReader br){
        this.br = br;
    }
    
    public void run(){
        try{
            String line = null;
            //不断从输入流中读取数据,并将这些数据打印输出
            while((line = br.readLine())!= null){
                System.out.println(line);
                /*
                 本例仅打印了从服务器端读到的内容。实际上,此处的情况可以更复杂:
                 如果我们希望客户端能看到聊天室的用户列表,则可以让服务器在
                 每次有用户登陆、用户退出时,将所有用户列表信息都向客户端发送一遍。
                 为了区分服务器发送的是聊天信息,还是用户列表,服务器也应该
                 在要发送的信息前、后都添加一定的协议字符串,客户端此处则根据协议
                 字符串的不同而进行不同的处理!
                 更复杂的情况:
                 如果两端进行游戏,则还有可能发送游戏信息,例如两端进行五子棋游戏,
                 则还需要发送下棋坐标信息等,服务器同样在这些下棋坐标信息前、后
                 添加协议字符串后再发送,客户端就可以根据该信息知道对手的下棋坐标。
                 */
            }
        }
        catch (IOException ex){
            ex.printStackTrace();
        }
        //使用finally块来关闭该线程对应的输入流
        finally{
            try{
                if (br != null){
                    br.close();
                }
            }
            catch (IOException ex){
                ex.printStackTrace();
            }
        }
    }
}

 

UDP协议网络编程
DatapramPacket,DatapgramSocket(单播),MulticastSocket(实现多点广播,组播)
通信两端各建立一个socket,但2个socket之间没有虚拟链路,这2个socket只是发送,接收数据报的对象。
DatagramSocket对象基于UDP协议的Socket,使用DatapgramSocket代表DatagramSocket发送,接收的数据报。
使用该种方式无需建立专用的虚拟连接,由于无需建立专用的连接,所以对于服务器的压力要比TCP小很多,所以也是一种常见的网络编程方式。

DatapramPacket的3个重要接口,获取发送者的IP和端口:
InetAddress sendIP = getPacket.getAddress();
int sendPort = getPacket.getPort();
SocketAddress sendAddress = getPacket.getSocketAddress();
// 通过数据报得到发送方的套接字地址(SocketAddress封装了InetAddress和代表端口的整数,也就是说SocketAddress可以同时代表IP地址和端口)

UDP测试例子:
服务端代码:

public class UdpServer{
    public static final int PORT = 30000;
    //定义每个数据报的最大大小为4K
    private static final int DATA_LEN = 4096;
    //定义该服务器使用的DatagramSocket
    private DatagramSocket socket = null;
    //定义接收网络数据的字节数组
    byte[] inBuff = new byte[DATA_LEN];
    //以指定字节数组创建准备接受数据的DatagramPacket对象
    private DatagramPacket inPacket = new DatagramPacket(inBuff , inBuff.length);
    //定义一个用于发送的DatagramPacket对象
    private DatagramPacket outPacket;
    //定义一个字符串数组,服务器发送该数组的的元素
    String[] books = new String[]{
        "轻量级J2EE企业应用实战",
        "基于J2EE的Ajax宝典",
        "Struts2权威指南",
        "ROR敏捷开发最佳实践"
    };
    
    public void init()throws IOException{
        try{
            //创建DatagramSocket对象
            socket = new DatagramSocket(PORT);
            
            //采用循环接受数据
            for (int i = 0; i < 1000 ; i++ ){
                //读取Socket中的数据,读到的数据放在inPacket所封装的字节数组里。
                socket.receive(inPacket);
                //判断inPacket.getData()和inBuff是否是同一个数组
                System.out.println(inBuff == inPacket.getData());
                //将接收到的内容转成字符串后输出
                System.out.println(new String(inBuff ,0 , inPacket.getLength()));
                
                //从字符串数组中取出一个元素作为发送的数据
                byte[] sendData = books[i % 4].getBytes();
                //以指定字节数组作为发送数据、以刚接受到的DatagramPacket的
                //源SocketAddress作为目标SocketAddress创建DatagramPacket。
                outPacket = new DatagramPacket(sendData ,sendData.length , inPacket.getSocketAddress());
                //发送数据
                socket.send(outPacket);    
            }
        }
        finally{
            if (socket != null){
                socket.close();
            }
        }
    }
    
    public static void main(String[] args) throws IOException{
        new UdpServer().init();
    }
}

客户端代码:

public class UdpClient{
    //定义发送数据报的目的地
    public static final int DEST_PORT = 30000;
    public static final String DEST_IP = "127.0.0.1";

    //定义每个数据报的最大大小为4K
    private static final int DATA_LEN = 4096;
    //定义该客户端使用的DatagramSocket
    private DatagramSocket socket = null;
    //定义接收网络数据的字节数组
    byte[] inBuff = new byte[DATA_LEN];
    //以指定字节数组创建准备接受数据的DatagramPacket对象
    private DatagramPacket inPacket = new DatagramPacket(inBuff , inBuff.length);
    //定义一个用于发送的DatagramPacket对象
    private DatagramPacket outPacket = null;
    
    public void init()throws IOException{
        try{
            //创建一个客户端DatagramSocket,使用随机端口
            socket = new DatagramSocket();
            //初始化发送用的DatagramSocket,它包含一个长度为0的字节数组
            outPacket = new DatagramPacket(new byte[0] , 0 ,InetAddress.getByName(DEST_IP) , DEST_PORT);
            //创建键盘输入流
            Scanner scan = new Scanner(System.in);
            //不断读取键盘输入
            while(scan.hasNextLine()){
                //将键盘输入的一行字符串转换字节数组
                byte[] buff = scan.nextLine().getBytes();
                //设置发送用的DatagramPacket里的字节数据
                outPacket.setData(buff);
                //发送数据报
                socket.send(outPacket);
                //读取Socket中的数据,读到的数据放在inPacket所封装的字节数组里。
                socket.receive(inPacket);
                System.out.println(new String(inBuff , 0 , inPacket.getLength()));
            }
        }
        finally{
            if (socket != null){
                socket.close();
            }
        }
    }

    public static void main(String[] args) throws IOException{
        new UdpClient().init();
    }
}

 

MulticastSocket实现多点广播:
DatagramSocket只允许数据报发送给指定的目标地址,而MulticastSocket可以将数据报以广播方式发送到数量不等的多个客户端。

若要使用多点广播时,则需要让一个数据报标有一组目标主机地址,当数据报发出后,整个组的所有主机都能收到该数据报。
IP多点广播(或多点发送)实现了将单一信息发送到多个接收者的广播,其思想是设置一组特殊网络地址作为多点广播地址,每一个多点广播地址都被看做一个组,当客户端需要发送、接收广播信息时,加入到该组即可。

广播是网络通信中常用的一种方式,将数据包一次发送给多台机器。广播本身也是UDP通信,只是发送时地址不是具体某一台机器的IP,而是标识一组计算机D类IP地址,凡是加入这个组的机器都可以接收到数据。IP协议为多点广播提供了这批特殊的IP地址,这些IP地址的范围是224.0.0.0至239.255.255.255,(D类IP地址)

joinGroup(InetAddress multicastAddr):将该MulticastSocket加入指定的多点广播地址。
leaveGroup(InetAddress multicastAddr):让该MulticastSocket离开指定的多点广播地址。
这2个方法对于MulticastSocket实现多点广播很重要,比如本地IP地址是10.10.121.73,那么我们必须要先创建一个MulticastSocket对象,然后将这个对象加入指定的多点广播地址,比如230.0.0.0,这样当我们接收到外部发送给230.0.0.0这个广播地址数据时候,因为我们的MulticastSocket对象加入了该广播地址这个组,所以10.10.121.73(就是我们创建MulticastSocket对象默认的本机地址)也可以接收到广播下来的数据了


在某些系统中,可能有多个网络接口。这可能会对多点广播带来问题,这时候程序需要在一个指定的网络接口上监听,通过调用setInterface可选择MulticastSocket所使用的网络接口;也可以使用getInterface方法查询MulticastSocket监听的网络接口。

如果创建仅用于发送数据报的MulticastSocket对象,则使用默认地址、随机端口即可。
但如果创建接收用的MulticastSocket对象,则该MulticastSocket对象必须具有指定端口,否则发送方无法确定发送数据报的目标端口。

DatagramSocket多一个setTimeToLive(int ttl)方法,该ttl参数设置数据报最多可以跨过多少个网络,
当ttl为0时,指定数据报应停留在本地主机;当ttl的值为1时,指定数据报发送到本地局域网;当ttl的值为32时,意味着只能发送到本站点的网络上;当ttl为64时,意味着数据报应保留在本地区;当ttl的值为128时,意味着数据报应保留在本大洲;当ttl为255时,意味着数据报可发送到所有地方;默认情况下,该ttl的值为1。

//让该类实现Runnable接口,该类的实例可作为线程的target
public class MulticastSocketTest implements Runnable{

    //使用常量作为本程序的多点广播IP地址
    private static final String BROADCAST_IP = "230.0.0.1";
    
    //使用常量作为本程序的多点广播目的的端口
    public static final int BROADCAST_PORT = 30000;
    
    //定义每个数据报的最大大小为4K
    private static final int DATA_LEN = 4096;

    
    //定义本程序的MulticastSocket实例
    private MulticastSocket socket = null;
    private InetAddress broadcastAddress = null;
    private Scanner scan = null;
    //定义接收网络数据的字节数组
    byte[] inBuff = new byte[DATA_LEN];
    //以指定字节数组创建准备接受数据的DatagramPacket对象
    private DatagramPacket inPacket = new DatagramPacket(inBuff , inBuff.length);
    //定义一个用于发送的DatagramPacket对象
    private DatagramPacket outPacket = null;
    
    public void init()throws IOException{
        try{
            //创建用于发送、接收数据的MulticastSocket对象
            //因为该MulticastSocket对象需要接收,所以有指定端口
            socket = new MulticastSocket(BROADCAST_PORT);//同时,使用本机默认的IP地址创建了MulticastSocket对象,比如192.168.1.1
            
            //将该socket加入指定的多点广播地址
            broadcastAddress = InetAddress.getByName(BROADCAST_IP);
            socket.joinGroup(broadcastAddress);//这样本机IP地址192.168.1.1创建的MulticastSocket对象就加入了广播地址230.0.0.1
            
            //设置本MulticastSocket发送的数据报被回送到自身
            socket.setLoopbackMode(false);
            
            //初始化发送用的DatagramSocket,它包含一个长度为0的字节数组
            outPacket = new DatagramPacket(new byte[0] , 0 ,broadcastAddress , BROADCAST_PORT);
            //启动以本实例的run()方法作为线程体的线程
            new Thread(this).start();
            
            //创建键盘输入流
            scan = new Scanner(System.in);
            //不断读取键盘输入
            while(scan.hasNextLine()){
                //将键盘输入的一行字符串转换字节数组
                byte[] buff = scan.nextLine().getBytes();
                //设置发送用的DatagramPacket里的字节数据
                outPacket.setData(buff);
                //发送数据报
                socket.send(outPacket);
            }
        }
        finally{
            socket.close();
        }
    }
    
    public void run(){
        try{
            while(true){
                //读取Socket中的数据,读到的数据放在inPacket所封装的字节数组里。
                socket.receive(inPacket);
                //打印输出从socket中读取的内容
                System.out.println("聊天信息:" + new String(inBuff , 0 , inPacket.getLength()));
            }
        }
        catch (IOException ex){
            ex.printStackTrace();
            try{
                if (socket != null){
                    //让该Socket离开该多点IP广播地址
                    socket.leaveGroup(broadcastAddress);
                    //关闭该Socket对象
                    socket.close();
                }
                System.exit(1);    
            }
            catch (IOException e){
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws IOException{
        new MulticastSocketTest().init();
    }
}

 

例子2:
//写了个MulticastSocket的程序,以作备忘:

import java.io.*;
import java.net.*;
import java.util.*;

public class MulticastClient{
    public static void main(String[] args) throws IOException{
        //创建MulticastSocket,默认本机Ip,端口是4446,需要接收,所以有指定端口
        MulticastSocket socket = new MulticastSocket(4446);
        
        //将该socket加入指定的多点广播地址230.0.0.1
        InetAddress address = InetAddress.getByName("230.0.0.1");
        socket.joinGroup(address);
        DatagramPacket packet;
        
        //发送数据包
        byte[] buf = "Hello,This is a member of multicast!".getBytes();
        packet = new DatagramPacket(buf, buf.length,address,4445);//组播发送数据,发送到指定端口4445
        socket.send(packet);
        
        //接收数据包并打印
        byte[] rev = new byte[512];
        packet = new DatagramPacket(rev, rev.length);
        socket.receive(packet);
        String received = new String(packet.getData()).trim();
        System.out.println("received: " + received);
        
        //退出组播组,关闭socket
        socket.leaveGroup(address);
        socket.close();
    }
}

---------------------------------------
import java.io.*;
import java.net.*;
import java.util.*;
public class AMulticastClient
{
    public static void main(String[] args) throws IOException{
        MulticastSocket socket = new MulticastSocket(4445);
        
        //将该socket加入指定的多点广播地址230.0.0.1(和上面的MulticastSocket加入同一个广播地址,属于同一组)
        InetAddress address = InetAddress.getByName("230.0.0.1");
        socket.joinGroup(address);
        
        DatagramPacket packet;
        //接收数据包
        byte[] buf = new byte[512];
        packet = new DatagramPacket(buf, buf.length);
        socket.receive(packet);
        
        //打印数据包
        String received = new String(packet.getData()).trim();
        System.out.println("received: " + received);
        
        //发送数据包
        byte[] sen=received.getBytes();
        packet=new DatagramPacket(sen,sen.length,address,4446);
        socket.send(packet);
        
       //退出组播组,关闭socket
        socket.leaveGroup(address);
        socket.close();
    }
}

 

例子3:
/**
* Description:该类用于网络通信,它包含了MulticastSocket实例和
* DatagramSocket实例,分别实现广播和私聊功能
*/
//聊天交换信息的工具类

public class ComUtil
{
    //使用常量作为本程序的多点广播IP地址
    private static final String BROADCAST_IP= "230.0.0.1";
    //使用常量作为本程序的多点广播目的的端口
    //DatagramSocket所用的的端口为该端口-1。
    public static final int BROADCAST_PORT = 30000;
    //定义每个数据报的最大大小为4K
    private static final int DATA_LEN = 4096;
    
    //定义本程序的MulticastSocket实例
    private MulticastSocket socket = null;
    //定义本程序私聊的Socket实例
    private DatagramSocket singleSocket = null;
    //定义广播的IP地址
    private InetAddress broadcastAddress = null;
    //定义接收网络数据的字节数组
    byte[] inBuff = new byte[DATA_LEN];
    //以指定字节数组创建准备接受数据的DatagramPacket对象
    private DatagramPacket inPacket = 
        new DatagramPacket(inBuff , inBuff.length);
    //定义一个用于发送的DatagramPacket对象
    private DatagramPacket outPacket = null;
    
    //聊天的主界面
    private LanChat lanTalk;
    
    //构造器,初始化资源
    public ComUtil(LanChat lanTalk)throws IOException , InterruptedException{
        this.lanTalk = lanTalk;
        //创建用于发送、接收数据的MulticastSocket对象
        //因为该MulticastSocket对象需要接收,所以有指定端口
        socket = new MulticastSocket(BROADCAST_PORT);
        //创建私聊用的DatagramSocket对象
        singleSocket = new DatagramSocket(BROADCAST_PORT + 1);
        broadcastAddress = InetAddress.getByName(BROADCAST_IP);
        //将该socket加入指定的多点广播地址
        socket.joinGroup(broadcastAddress);
        //设置本MulticastSocket发送的数据报被回送到自身
        socket.setLoopbackMode(false);
        
        //初始化发送用的DatagramSocket,它包含一个长度为0的字节数组
        outPacket = new DatagramPacket(new byte[0] , 0 ,broadcastAddress , BROADCAST_PORT);
        //启动两个读取网络数据的线程
        new ReadBroad().start();
         Thread.sleep(1);
        new ReadSingle().start();
    }
    
    //广播消息的工具方法
    public void broadCast(String msg){
        try{
            //将msg字符串转换字节数组
            byte[] buff = msg.getBytes();
            //设置发送用的DatagramPacket里的字节数据
            outPacket.setData(buff);
            //发送数据报
            socket.send(outPacket);
        }
        catch (IOException ex){
            ex.printStackTrace();
            if (socket != null){
                //关闭该Socket对象
                socket.close();
            }
            JOptionPane.showMessageDialog(null, 
                "发送信息异常,请确认30000端口空闲,且网络连接正常!"
                , "网络异常", JOptionPane.ERROR_MESSAGE);
            System.exit(1);
        }
    }
    
    //定义向单独用户发送消息的方法
    public void sendSingle(String msg , SocketAddress dest){
        try{
            //将msg字符串转换字节数组
            byte[] buff = msg.getBytes();buff , buff.length , dest);
            singleSocket.send(packet);
        }
        catch (IOException ex){
            ex.printStackTrace();
            if (singleSocket != null){
                //关闭该Socket对象
                singleSocket.close();
            }
            JOptionPane.showMessageDialog(null, 
                "发送信息异常,请确认30001端口空闲,且网络连接正常!"
                , "网络异常", JOptionPane.ERROR_MESSAGE);
            System.exit(1);
        }
    }
    
    //不断从DatagramSocket中读取数据的线程
    class ReadSingle extends Thread{
        //定义接收网络数据的字节数组
        byte[] singleBuff = new byte[DATA_LEN];
        private DatagramPacket singlePacket = new DatagramPacket(singleBuff , singleBuff.length);
        public void run(){
            while (true){
                try{
                    //读取Socket中的数据,读到的数据放在inPacket所封装的字节数组里。
                    singleSocket.receive(singlePacket);
                    //处理读到的信息
                    lanTalk.processMsg(singlePacket , true);
                }
                catch (IOException ex){
                    ex.printStackTrace();
                    if (singleSocket != null){
                        singleSocket.close();
                    }
                    JOptionPane.showMessageDialog(null,
                        "接收信息异常,请确认30001端口空闲,且网络连接正常!"
                        , "网络异常", JOptionPane.ERROR_MESSAGE);
                    System.exit(1);
                }                
            }
        }
    }
    
    //持续读取MulticastSocket的线程
    class ReadBroad extends Thread{
        public void run(){
            while (true){
                try{
                    //读取Socket中的数据,读到的数据放在inPacket所封装的字节数组里。
                    socket.receive(inPacket);
                    //打印输出从socket中读取的内容
                    String msg = new String(inBuff , 0 , inPacket.getLength());
                    //读到的内容是在线信息
                    if (msg.startsWith(YeekuProtocol.PRESENCE) && msg.endsWith(YeekuProtocol.PRESENCE)){
                        String userMsg = msg.substring(2 , msg.length() - 2);
                        String[] userInfo = userMsg.split(YeekuProtocol.SPLITTER);
                        UserInfo user = new UserInfo(userInfo[1] , userInfo[0] , 
                            inPacket.getSocketAddress(), 0);
                        //控制是否需要添加该用户的旗标
                        boolean addFlag = true;
                        ArrayList<Integer> delList = new ArrayList<Integer>();
                        //遍历系统中已有的所有用户,该循环必须循环完成
                        for (int i = 1 ; i < lanTalk.getUserNum() ; i++ ){
                            UserInfo current = lanTalk.getUser(i);
                            //将所有用户失去联系的次数加1
                            current.setLost(current.getLost() + 1);
                            //如果该信息由指定用户发送过来
                            if (current.equals(user)){
                                current.setLost(0);
                                //设置该用户无需添加
                                addFlag = false;
                            }
                            if (current.getLost() > 2){
                                delList.add(i);
                            }
                        }
                        //删除delList中的所有索引对应的用户
                        for (int i = 0; i < delList.size() ; i++){
                            lanTalk.removeUser(delList.get(i));
                        }
                        if (addFlag){
                            //添加新用户
                            lanTalk.addUser(user);
                        }                        
                    }
                    //读到的内容是公聊信息
                    else{
                        //处理读到的信息
                        lanTalk.processMsg(inPacket , false);
                    }
                }
                catch (IOException ex){
                    ex.printStackTrace();
                    if (socket != null){
                        //关闭该Socket对象
                        socket.close();
                    }
                    JOptionPane.showMessageDialog(null, 
                        "接收信息异常,请确认30000端口空闲,且网络连接正常!"
                        , "网络异常", JOptionPane.ERROR_MESSAGE);
                    System.exit(1);
                }
            }
        }
    }
}

 

关于UrlConnection连接和Socket连接的区别

只知道其中的原理如下:
抽象一点的说,Socket只是一个供上层调用的抽象接口,隐藏了传输层协议的细节。
urlconnection 基于Http协议,Http协议是应用层协议,对传输层Tcp协议进行了封装,是无状态协议,不需要你去考虑线程、同步、状态管理等,
内部是通过socket进行连接和收发数据的,不过一般在数据传输完成之后需要关闭socket连接。
使用UrlConnection比直接使用Socket要简单的多,不用关心状态和线程管理。
直接使用Socket进行网络通信得考虑线程管理、客户状态监控等,但是不用发送头信息等,更省流量。

分析源码如下 :
url.openConnection()调用的是strmHandler.openConnection(this);
而strmHandler是URLStreamHandler接口的子类的实例。
抽象类 URLStreamHandler 是所有流协议处理程序的通用超类,可以通过不同 protocol 的 URL 实例,产生 java.net.URLConnection 对象。
由于context和handler是null,所以最终根据具体的协议调用URL类中的setupStreamHandler()方法对strmHandler进行初始化。

将调用下面的Handler类的实例的openConnection(URL u)方法。

protected URLConnection openConnection(URL u) throws IOException {  
    return new HttpURLConnectionImpl(u, getDefaultPort());  
}  
  
protected URLConnection openConnection(URL u, Proxy proxy)  
        throws IOException {  
    if (null == u || null == proxy) {  
        throw new IllegalArgumentException(Messages.getString("luni.1B"));   
    }  
    return new HttpURLConnectionImpl(u, getDefaultPort(), proxy);  
} 

HttpConnection 部分源码如下:

private SSLSocket sslSocket;  
private InputStream inputStream;  
private OutputStream outputStream;  
private InputStream sslInputStream;  
private OutputStream sslOutputStream;  
  
private HttpConfiguration config;  
  
 public HttpConnection(HttpConfiguration config, int connectTimeout) throws IOException {  
     this.config = config;  
    String hostName = config.getHostName();  
    int hostPort = config.getHostPort();  
     Proxy proxy = config.getProxy();  
     if(proxy == null || proxy.type() == Proxy.Type.HTTP) {  
        socket = new Socket();  
     } else {  
         socket = new Socket(proxy);  
     }  
     socket.connect(new InetSocketAddress(hostName, hostPort), connectTimeout);  
 }  

现在UrlConnection连接和Socket连接的区别应该十分清楚了吧。

参考UrlConnection连接和Socket连接的区别
http://zhoujianghai.iteye.com/blog/1195988

 

参考:
java socket 属性设置
http://blog.csdn.net/fastthinking/article/details/10930193

Java Socket 几个重要的TCP/IP选项解析(一)
http://elf8848.iteye.com/blog/1739598
http://elf8848.iteye.com/blog/1739604

JAVA Socket超时浅析
http://blog.csdn.net/sureyonder/article/details/5633647

Java网络编程之传输控制协议
http://blog.csdn.net/xiaojianpitt/article/details/2767213
 
Socket 关于设置Socket连接超时时间
http://cuisuqiang.iteye.com/blog/1725348

SO_KEEPALIVE选项 (LINUX)
http://blog.chinaunix.net/uid-26575352-id-3483808.html

LINUX C网络编程中的心跳机制
http://blog.chinaunix.net/uid-9688646-id-4054244.html

posted on 2013-03-27 15:56  Cynthia&Sky  阅读(741)  评论(0编辑  收藏  举报

导航