Socket通信

网络基础

计算机网络

计算机网络是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。
网络编程就是用来实现网络互连的不同计算机上运行的程序间可以进行数据交换。

网络通信

两台计算机通过网络进行通信,需要具备以下条件
IP地址:计算机唯一标识
协议:通讯语言/规则
端口:程序入口,不同进程的标识

TCP/IP协议

TCP/IP是目前世界上应用最为广泛的协议
是以TCP和IP为基础的不同层次上多个协议的集合
也称:TCP/IP协议族或TCP/IP协议栈
TCP:Transmission Control Protocol 传输控制协议
IP:Internet Protocol 互联网协议

TCP/IP模型
相关概念

IP地址:实现网络中不同计算机之间的通信,每台机器都必须有一个唯一的标识
端口:用于区分不同应用程序,端口号范围0-65535,其中0-1023是系统保留的端口号
IP地址和端口号组成了所谓的SocketSocket是网络上运行的程序之间双向通信链路的终结点,是TCP和UDP的基础

java中的网络支持

针对网络通信的不同层次,Java提供的网络功能有四大类:在 java.net包下
InetAddress: 用于标识网络上的硬件资源
URL: 统一资源定位符通过URL可以直接读取或写入网络上的数据
Socket: 使用TCP协议实现网络通信的Socket相关的类
Datagram: 使用UDP协议,将数据保存在数据报中,通过网络进行通信

InetAddress类

此类表示互联网协议(IP)地址

常用方法
示例
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;

public class InetAddressDemo {

    public static void main(String[] args) throws UnknownHostException {
        // 获取本机的IntetAddress实例
        InetAddress address = InetAddress.getLocalHost();
        System.out.println("计算机名:" + address.getHostName());
        System.out.println("IP地址:" + address.getHostAddress());
        byte[] bytes = address.getAddress();// 获取字节数组形式的IP地址
        System.out.println("字节数组:" + Arrays.toString(bytes));
        System.out.println(address);// 直接输出InetAddress对象

        // InetAddress address2 = InetAddress.getByName("DESKTOP-JQI6O11");
        InetAddress address2 = InetAddress.getByName("192.168.84.1");
        System.out.println("计算机名:" + address.getHostName());
        System.out.println("IP地址:" + address.getHostAddress());
        /*
         * 输出结果 
         * 计算机名:DESKTOP-JQI6O11 
         * IP地址:192.168.84.1 
         * 字节数组:[-64, -88, 84, 1]
         * DESKTOP-JQI6O11/192.168.84.1 
         * 计算机名:DESKTOP-JQI6O11 
         * IP地址:192.168.84.1
         */
    }
}

URL类

URL(Uniform Resource Locator) 统一资源定位符,表示Internet上某一资源的地址
URL主要由两部分组成:协议名称和资源名称,中间用冒号隔开

构造方法
常用方法
示例
import java.net.MalformedURLException;
import java.net.URL;

public class URLDemo {

    public static void main(String[] args) {
        try {
            URL silly = new URL("http://www.silly.com");
            //?后面代表参数,#后面表示锚点
            URL url = new URL(silly, "/index.html?username=tom#test");
            System.out.println("协议:"+url.getProtocol());
            System.out.println("主机:"+url.getHost());
            //如果未指定端口号,则使用默认http协议的端口号80进行访问
            //对于缺省端口号getPort()方法统一返回-1
            System.out.println("端口:"+url.getPort());
            System.out.println("文件路径:"+url.getPath());
            System.out.println("文件名:"+url.getFile());
            System.out.println("相对路径:"+url.getRef());
            System.out.println("查询字符串:"+url.getQuery());
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
        /*
        协议:http
        主机:www.silly.com
        端口:-1
        文件路径:/index.html
        文件名:/index.html?username=tom
        相对路径:test
        查询字符串:username=tom
        */
    }
}
使用URL读取网页内容

通过URL对象的openStream()方法可以得到指定资源的输入流
通过输入流可以读取、访问网络上的数据

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;

public class URLDemo2 {
    
    public static void main(String[] args) {
        
        //创建url实例
        try {
            URL url = new URL("http://www.baidu.com");
            //通过url的openStream方法获取url对象所表示的资源的字节输入流
            InputStream is = url.openStream();
            //将字节输入流转化为字符输入流
            InputStreamReader isr = new InputStreamReader(is,"utf-8");
            //为字符输入流添加缓冲
            BufferedReader br = new BufferedReader(isr);
            String data = br.readLine(); //读取数据
            while(data != null){ //循环读取数据
                System.out.println(data);//输出数据
                data = br.readLine();
            }
            br.close();
            is.close();
            isr.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /*
     打印的内容就是百度首页的html文档
    */
}

Socket通信简介

TCP协议是面向连接、可靠的、有序的,以字节流的方式发送数据
基于TCP协议实现网络通信的类
— 客户端的Socket
— 服务器端的ServerSocket

通讯模型
Socket通信实现步骤
  1. 创建ServerSocket和Socket
  2. 打开连接到Socket的输入/输出流
  3. 按照协议对Socket进行读/写操作
  4. 关闭输入输出流、关闭Socket

ServerSocket类

此类实现服务器套接字。服务器套接字等待请求通过网络传入并进行相关操作

构造方法

ServerSocket(int port):创建绑定到特定端口的服务器套接字

常用方法

Socket类

此类实现客户端套接字,套接字是两台机器间通信的端点

构造方法

Socket(String host, int port):创建一个套接字并将其连接到指定主机上的指定端口号

常用方法

DatagramPacket类

此类表示数据报包,数据报包用来实现无连接包投递服务

构造方法
常用方法

DatagramSocket类

此类表示用来发送和接收数据报包的套接字,数据报套接字是包投递服务的发送或接收点

构造方法
常用方法

基于TCP的socket通信

服务端
  1. 创建ServerSocket对象,绑定监听端口
  2. 通过accept()方法监听客户端请求
  3. 连接建立后,通过输入流读取客户端发送的请求信息
  4. 通过输出流向客户端发送响应信息
  5. 关闭相关资源
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
/**
 * 基于tcp协议的socket通信,实现用户登录
 * 服务器端
 */
public class Server {

    public static void main(String[] args) {
        
        try {
            //1.创建服务器端Socket,指定绑定的端口
            ServerSocket server = new ServerSocket(8888);
            //2.调用accept()方法开始监听,等待客户端的连接
            System.out.println("***服务器启动,等待客户端连接***");
            Socket socket = server.accept();
            //3.获取输入流,并读取客户端信息
            InputStream in = socket.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(in));//转换为缓冲字符流
            String info = null;
            while((info = br.readLine()) != null){//循环读取客户端信息
                System.out.println("我是服务器,客户端说:"+info);
            }
            //此处必须关闭输入流
            socket.shutdownInput();
            //4.获取输出流,响应客户端的请求
            OutputStream os = socket.getOutputStream();
            PrintWriter pw = new PrintWriter(os);//包装为打印流
            pw.write("欢迎您!");
            pw.flush(); //将缓冲输出
            socket.shutdownOutput();
            //5.关闭资源
            socket.close();
            server.close();
        } catch (IOException e) {
            e.printStackTrace();
        }   
    }
}
客户端
  1. 创建Socket对象,指明需要连接的服务器的地址和端口号
  2. 连接建立后,通过输出流向服务器端发送请求信息
  3. 通过输入流获取服务器响应的信息
  4. 关闭相关资源
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
/**
 * 基于TCP的socket通信,实现用户登录
 * 客户端
 */
public class Client {

    public static void main(String[] args) {
        try {
            // 1.创建客户端Socket,指定服务器地址和端口
            Socket socket = new Socket("127.0.0.1", 8888);
            // 2.获取输出流,向服务器端发送信息
            OutputStream os = socket.getOutputStream();
            PrintWriter pw = new PrintWriter(os);// 将输出流包装为打印流
            pw.write("用户名:alice;密码:123456");
            pw.flush();
            // 此处必须关闭输出流
            //注意不能使用os.close()或者pw.close()方法关闭流,会导致socket也被关闭
            socket.shutdownOutput();
            // 3.获取输入流,并读取服务器端的响应信息
            InputStream in = socket.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(in));
            String info = null;
            while ((info = br.readLine()) != null) {
                System.out.println("我是客户端,服务器说:" + info);
            }
            socket.shutdownInput();
            // 4.关闭资源
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
运行结果

首先启动服务端,然后启动客户端

# 服务端打印内容
***服务器启动,等待客户端连接***
我是服务器,客户端说:用户名:alice;密码:123456
# 客户端打印内容
我是客户端,服务器说:欢迎您!
多线程服务器

应用多线程来实现服务器与多客户端之间的通信基本步骤

  1. 服务器端创建ServerSocket,循环调用accept() 等待客户端连接
  2. 客户端创建一个socket并请求和服务器端连接
  3. 服务器端接受客户端请求,创建socket与该客户建立专线连接
  4. 建立连接的两个socket在一个单独的线程上对话
  5. 服务器端继续等待新的连接
创建服务端线程处理类
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
/**
 * 服务器线程处理类
 */
public class ServerThread implements Runnable{
    
    // 和本线程相关的Socket
    private Socket socket;
    
    public ServerThread(Socket socket) {
        this.socket = socket;
    }
    //线程执行的操作,响应客户端的请求
    public void run() {
        try {
            //获取输入流,并读取客户端信息
            InputStream in = socket.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(in));//转换为缓冲字符流
            String info = null;
            while((info = br.readLine()) != null){//循环读取客户端信息
                System.out.println("我是服务器,客户端说:"+info);
            }
            //此处必须关闭输入流
            socket.shutdownInput();
            //获取输出流,响应客户端的请求
            OutputStream os = socket.getOutputStream();
            PrintWriter pw = new PrintWriter(os);//包装为打印流
            pw.write("欢迎您!");
            pw.flush();//将缓冲输出
            socket.shutdownOutput();
            //关闭资源
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(socket!=null)
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }
    }
}
服务端改写
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
/**
 * 
 * 基于tcp协议的socket通信,实现用户登录
 * 服务器端多线程实现
 */
public class Server {

    public static void main(String[] args) {
        
        try {
            //创建服务器端Socket,指定绑定的端口
            ServerSocket server = new ServerSocket(8888);
            Socket socket = null;
            //记录计数连接过服务器的客户端数量
            int count = 0;
            System.out.println("***服务器启动,等待客户端连接***");
            //循环监听等待客户端的连接
            while(true){
                //调用accept()方法开始监听,等待客户端的连接
                socket = server.accept();
                //创建一个新的线程
                ServerThread serverThread = new ServerThread(socket);
                Thread thread = new Thread(serverThread);
                thread.setPriority(4); //设置线程优先级,范围为[1-10],默认值为5
                //启动线程
                thread.start();
                count++;//统计数量
                System.out.println("连接过服务器端的客户端的数量:"+count);
                InetAddress address = socket.getInetAddress();
                System.out.println("当前客户端的IP:"+address.getHostAddress());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
运行结果

首先启动服务端,然后启动一个客户端,修改客户端写入的内容,再次启动一个客户端

# 服务端打印内容
***服务器启动,等待客户端连接***
连接过服务器端的客户端的数量:1
当前客户端的IP:127.0.0.1
我是服务器,客户端说:用户名:alice;密码:123456
连接过服务器端的客户端的数量:2
当前客户端的IP:127.0.0.1
我是服务器,客户端说:用户名:tom;密码:555666

基于UDP的socket通信

UDP协议(用户数据报协议)是无连接、不可靠的、无序的,UDP协议以数据报作为数据传输的载体
进行数据传输时,首先需要将要传输的数据定义成数据报(Datagram),在数据报中指明数据所要到达的Socket(主机地址和端口号),然后再将数据报发送出去

服务器端
  1. 创建DatagramSocket,指定端口号
  2. 创建DatagramPacket
  3. 接收客户端发送的数据信息
  4. 读取数据
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/**
 * 服务器端,实现基于UDP的用户登陆
 */
public class UDPServer {
    
    public static void main(String[] args) throws IOException {
        
        /*********接收客户端发送的数据*********/
        // 1.创建服务器端DatagramSocket,指定端口
        DatagramSocket socket = new DatagramSocket(8800);
        // 2.创建数据报,用于接收客户端发送的数据
        byte[] data = new byte[1024];// 创建字节数组,指定接收的数据包的大小
        DatagramPacket packet = new DatagramPacket(data, data.length);
        // 3.接收客户端发送的数据
        System.out.println("***服务器端已经启动,等待客户端发送数据***");
        socket.receive(packet);// 此方法在接收到数据报之前会一直阻塞
        // 4.读取数据
        String info = new String(packet.getData(), 0, packet.getLength());
        System.out.println("我是服务器,客户端说:" + info);
        
        /*********向客户端响应数据*********/
        // 1.定义客户端的地址、端口号、数据
        InetAddress address = packet.getAddress();
        int port = packet.getPort();
        byte[] data2 = "欢迎您!".getBytes();
        // 2.创建数据报,包含响应的数据信息
        DatagramPacket packet2 = new DatagramPacket(data2, data2.length, address, port);
        // 3.响应客户端
        socket.send(packet2);
        // 4.关闭资源
        socket.close();
    }
}
客户端
  1. 定义发送信息
  2. 创建DatagramPacket,包含将要发送的信息
  3. 创建DatagramSocket
  4. 发送数据
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/**
 * 客户端,实现基于UDP的用户登陆
 */
public class UDPClient {
    
    public static void main(String[] args) throws IOException {
        
        /*********向服务器端发送数据*********/
        // 1.定义服务器的地址、端口号、数据
        InetAddress address = InetAddress.getByName("localhost");
        int port = 8800;
        byte[] data = "用户名:admin;密码:123".getBytes();
        // 2.创建数据报,包含发送的数据信息
        DatagramPacket packet = new DatagramPacket(data, data.length, address, port);
        // 3.创建DatagramSocket对象
        DatagramSocket socket = new DatagramSocket();
        // 4.向服务器端发送数据报
        socket.send(packet);

        /*********接收服务器端响应的数据*********/
        // 1.创建数据报,用于接收服务器端响应的数据
        byte[] data2 = new byte[1024];
        DatagramPacket packet2 = new DatagramPacket(data2, data2.length);
        // 2.接收服务器响应的数据
        socket.receive(packet2);
        // 3.读取数据
        String reply = new String(data2, 0, packet2.getLength());
        System.out.println("我是客户端,服务器说:" + reply);
        // 4.关闭资源
        socket.close();
    }
}
运行结果

首先启动服务端,然后启动客户端

# 服务端打印内容
***服务器端已经启动,等待客户端发送数据***
我是服务器,客户端说:用户名:admin;密码:123
# 客户端打印内容
我是客户端,服务器说:欢迎您!

Socket总结

多线程优先级

在使用多线程处理多客户端通信时,未设置优先级时可能会导致运行时速度非常慢,可以降低优先级

输入和输出流的关闭
  1. 在同时使用输入和输出流时,socket只能接受一个流,要么输入要么输出。完成输入或输出流之后必须关闭(调用socket.shutdownInput()socket.shutdownInput())让下一个流进来,此时socket仍然是连接状态。
  2. 对于同一个socket,如果直接关闭输入或者输出流(调用close()方法),则与该输入或者输出流关联的socket也会被关闭,所以一般不用关闭流,直接关闭socket即可。
使用TCP通信传输对象

真实的开发中都是以对象作为传输数据,可以使用ObjectOutputStreamObjectInputStream完成对象传输
1.定义User类,实现序列化接口

public class User implements Serializable{ 
    private String username;
    private String password;
    //省略getter和setter
    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }
}

2.修改Client发送逻辑

3.修改ServerThread处理逻辑

Socket编程传递文件

这里简单演示服务器向客户端发送文件
1.修改ServerThread处理逻辑

2.修改Client逻辑

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269