网络编程,意思就是编写的应用程序可以与网络上其他设备中的应用程序进行数据交互。Java提供的网络编程的解决方案都是在java.net包下。在正式学习Java网络编程技术之前,我们还需要学习一些网络通信的前置知识理论知识
网络通信的基本架构。通信的基本架构主要有两种形式:一种是CS架构(Client 客户端/Server服务端)、一种是BS架构(Brower 浏览器/Server服务端)。
CS架构的特点:CS架构需要用户在自己的电脑或者手机上安装客户端软件,然后由客户端软件通过网络连接服务器程序,由服务器把数据发给客户端,客户端就可以在页面上看到各种数据了。
BS架构的特点:BS架构不需要开发客户端软件,用户只需要通过浏览器输入网址就可以直接从服务器获取数据,并由服务器将数据返回给浏览器,用户在页面上就可以看到各种数据了。Java的程序员,以后从事的工作方向主要还是BS架构的。
网络编程三要素
三要素分别是IP地址、端口号、通信协议
**IP地址**:表示设备在网络中的地址,是网络中设备的唯一标识
**端口号**:应用程序在设备中唯一的标识
**协议**:连接和数据在网络中传输的规则
假设现在要从一台电脑中的微信上,发一个消息到其他电脑的微信上,流程如下
1.先通过ip地址找到对方的电脑
2.再通过端口号找到对方的电脑上的应用程序
3.按照双方约定好的规则发送、接收数据
IP地址
IP(Ineternet Protocol)全称互联网协议地址,是分配给网络设备的唯一表示。IP地址分为:IPV4地址、IPV6地址
IPV4地址由32个比特位(4个字节)组成,但是由于采用二进制太不容易阅读了,于是就将每8位看成一组,把每一组用十进制表示(叫做点分十进制表示法)。所以就有了我们经常看到的IP地址形式,如:192.168.1.66
如果想查看本机的IP地址,可以在命令行窗口,输入ipconfig命令查看。
经过不断的发展,现在越来越多的设备需要联网,IPV4地址已经不够用了,所以扩展出来了IPV6地址。
IPV6采用128位二进制数据来表示(16个字节),号称可以为地球上的每一粒沙子编一个IP地址,现在的网络设备,一般IPV4和IPV6地址都是支持的。
IPV6比较长,为了方便阅读,每16位编成一组,每组采用十六进制数据表示,然后用冒号隔开(称为冒分十六进制表示法),如下图所示
为什么有了IPV6还有很多地方用的IPV4呢?因为很多老设备还用的是IPV4,不可能一下子都舍弃,现在慢慢也会过渡到IPV6。
公网IP与内网IP
公网IP:是可以连接互联网的IP地址;内网IP:也叫局域网IP,只能组织机构内部使用。
192.168.开头的就是常见的局域网地址,范围即为192.168.0.0–192.168.255.255,专门为组织机构内部使用。
介绍一下和IP地址相关的一个东西,叫做域名。我们在浏览器上访问某一个网站是,就需要在浏览器的地址栏输入网址,这个网址的专业说法叫做域名。比如:http://www.baidu.com。
域名和IP其实是一一对应的,由运营商来管理域名和IP的对应关系。我们在浏览器上敲一个域名时,首先由运营商的域名解析服务,把域名转换为ip地址,再通过IP地址去访问对应的服务器设备。
关于IP地址,还有一个特殊的地址需要我们记住一下。就是我们在学习阶段进行测试时,经常会自己给自己消息,需要用到一个本地回送地址:127.0.0.1,或者localhost,也是代表这个。
两个和IP地址相关的命令
ipconfig: 查看本机的ip地址
pring 域名/ip 检测当前电脑与指定的ip是否连通
ping命令出现以下的提示,说明网络是通过的
InetAddress类
Java中也有一个类用来表示IP地址,这个类是InetAddress类。我们在开发网络通信程序的时候,可能有时候会获取本机的IP地址,以及测试与其他地址是否连通,这个时候就可以使用InetAddress类来完成。
演示上面几个方法的效果
public class InetAddressTest {
public static void main(String[] args) throws Exception {
// 1、获取本机IP地址对象的
InetAddress ip1 = InetAddress.getLocalHost();
System.out.println(ip1.getHostName());
System.out.println(ip1.getHostAddress());
// 2、获取指定IP或者域名的IP地址对象。
InetAddress ip2 = InetAddress.getByName("www.baidu.com");
System.out.println(ip2.getHostName());
System.out.println(ip2.getHostAddress());
// ping www.baidu.com
System.out.println(ip2.isReachable(6000));
}
}
端口号
端口号:指的是计算机设备上运行的应用程序的标识,被规定为一个16位的二进制数据,范围(0~65535)
端口号分为以下几类(了解)
周知端口:0~1023,被预先定义的知名应用程序占用(如:HTTP占用80,FTP占用21)
注册端口:1024~49151,分配给用户经常或者某些应用程序
动态端口:49152~65536,之所以称为动态端口,是因为它一般不固定分配给某进程,而是动态分配的。
同一个计算机设备中,不能出现两个应用程序,用同一个端口号
注意:我们自己开发的程序一般选择使用注册端口,且一个设备中不能出现两个程序的端口号一样,否则出错。
协议
网络上通信的设备,事先规定的连接规则,以及传输数据的规则被称为网络通信协议。指定一个规则,大家都遵守这个规则,才能进行数据通信。只要按照OSI网络参考模型制造的设备,就可以在国际互联网上互联互通。其中传输层有两个协议,是我们今天会接触到的(UDP协议、TCP协议)
UDP协议特点
TCP协议特点
为什么是三次握手?因为三次握手就可以保证双方确保对方收发消息都没问题,才能建立起可靠连接。并且传输数据会进行确认,以保证数据传输的可靠性,发消息没有收到相应会认为发送失败进行重发。
四次挥手如下图所示:目的是确保双方数据的收发已经完成,没有数据丢失
入门案例一发一收
首先学习基于UDP协议通信的代码编写。
UDP是面向无连接的、不需要确认双方是否存在,所以它是不可靠的协议。Java提供了一个类叫DatagramSocket来完成基于UDP协议的收发数据。使用DatagramSocket收发数据时,数据要以数据包DatagramPacket的形式体现,一个数据包限制在64KB以内。
下面我们看一个案例,需要有两个程序,一个表示客户端程序,一个表示服务端程序。
需求:客户端程序发一个字符串数据给服务端,服务端程序接收数据并打印。
客户端程序
/**
* 目标:完成UDP通信快速入门:实现1发1收。
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1、创建客户端对象(发数据出去的人)
DatagramSocket socket = new DatagramSocket(7777);//可以无参构造也可以人为的为客户端分配端口号
// 2、创建数据包对象封装要发出去的数据
/* public DatagramPacket(byte buf[], int length,
InetAddress address, int port)
参数一:封装要发出去的数据。
参数二:发送出去的数据大小(字节个数)
参数三:服务端的IP地址(找到服务端主机)
参数四:服务端程序的端口。
*/
byte[] bytes = "我是快乐的客户端,我爱你abc".getBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length
, InetAddress.getLocalHost(), 6666);
//这里服务端就在自己电脑上 所以填自己ip就行了
// 3、开始正式发送这个数据包的数据出去了
socket.send(packet);
System.out.println("客户端数据发送完毕~~~");
socket.close(); // 关闭通信管道 释放资源!
}
}
服务端程序
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("----服务端启动----");
// 1、创建一个服务端对象(创建一个接数据的人) 注册端口 端口号必须与发送代码中一致
DatagramSocket socket = new DatagramSocket(6666);
// 2、创建一个数据包对象,用于接收数据的
byte[] buffer = new byte[1024 * 64]; // 64KB,保证接收到一个数据包里的全部数据.
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
// 3、开始正式使用数据包来接收客户端发来的数据
socket.receive(packet);
// 4、从字节数组中,把接收到的数据直接打印出来
// 接收多少就倒出多少 要不然多输出了没意义的数据
// 获取本次数据包接收了多少数据。
int len = packet.getLength();
String rs = new String(buffer, 0 , len);
System.out.println(rs);// 打印
//可以知道是哪个客户端给它发的消息 获取客户端ip地址和端口号
System.out.println(packet.getAddress().getHostAddress());
System.out.println(packet.getPort());
socket.close(); // 释放资源
}
}
注意:是先运行客户端程序还是先运行服务端程序呢?答案是应当先运行服务端程序,先运行客户端程序,因为他发送udp数据包之后就不管了,就关闭程序了,这个时候也没有人接,数据包就丢失了。应当先运行服务端程序,程序运行到socket.receive(packet);会等待数据包的到来所以程序不会终止,这个时候再运行客户端程序,就完成了数据收发。
UDP通信代码——多发多收
客户端程序
/**
* 目标:完成UDP通信快速入门:实现客户端反复的发。
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1、创建客户端对象(发数据出去的人)
DatagramSocket socket = new DatagramSocket();
// 2、创建数据包对象封装要发出去的数据
/* public DatagramPacket(byte buf[], int length,
InetAddress address, int port)
参数一:封装要发出去的数据。
参数二:发送出去的数据大小(字节个数)
参数三:服务端的IP地址(找到服务端主机)
参数四:服务端程序的端口。
*/
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请说:");
String msg = sc.nextLine();
// 一旦发现用户输入的exit命令,就退出客户端
if("exit".equals(msg)){
System.out.println("欢迎下次光临!退出成功!");
socket.close(); // 释放资源
break; // 跳出死循环
}
byte[] bytes = msg.getBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length
, InetAddress.getLocalHost(), 6666);
// 3、开始正式发送这个数据包的数据出去了
socket.send(packet);
}
}
}
服务端程序
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("----服务端启动----");
// 1、创建一个服务端对象(创建一个接数据的人) 注册端口
DatagramSocket socket = new DatagramSocket(6666);
// 2、创建一个数据包对象,用于接收数据的
byte[] buffer = new byte[1024 * 64]; // 64KB.
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
while (true) {
// 3、开始正式使用数据包来接收客户端发来的数据
socket.receive(packet);
// 4、从字节数组中,把接收到的数据直接打印出来
// 接收多少就倒出多少
// 获取本次数据包接收了多少数据。
int len = packet.getLength();
String rs = new String(buffer, 0 , len);
System.out.println(rs);
System.out.println(packet.getAddress().getHostAddress());
System.out.println(packet.getPort());
System.out.println("--------------------------------------");
}
}
}
注意这里服务端是没有关闭代码的,在正常开发的系统中服务端肯定是不关的,比如京东淘宝肯定是要源源不断的接受客户端发送的数据的。
先运行服务端,再运行客户端,可以实现反复发反复收。
这里还要注意一个问题,能不能多个不同的客户端同时向这一个服务端发送数据呢?答案是可以的。
我们可以运行多次客户端程序不就相当于多个客户端吗?而且我们创建客户端对象采用无参构造,系统自动分配不会出现端口冲突。
但是要注意,idea默认一个程序只能启动一次:
我们需要改一些设置,允许创造多个实例:
应用后,就能创造多个客户端同时向一个服务端发送数据了。
Java提供了一个java.net.Socket类来完成TCP通信。我们先讲一下Socket完成TCP通信的流程,再讲代码怎么编写就很好理解了。
1.当创建Socket对象时,就会在客户端和服务端创建一个数据通信的管道,在客户端和服务端两边都会有一个Socket对象来访问这个通信管道。
2.现在假设客户端要发送一个“在一起”给服务端,客户端这边先需要通过Socket对象获取到一个字节输出流,通过字节输出流写数据到服务端
3.然后服务端这边通过Socket对象可以获取字节输入流,通过字节输入流就可以读取客户端写过来的数据,并对数据进行处理。
4.服务端处理完数据之后,假设需要把“没感觉”发给客户端端,那么服务端这边再通过Socket获取到一个字节输出流,将数据写给客户端
5.客户端这边再获取输入流,通过字节输入流来读取服务端写过来的数据。
入门案例一发一收
TCP客户端
下面我们写一个客户端,用来往服务端发数据。由于原始的字节流不是很好用,这里根据我的经验,将原始的OutputStream包装为DataOutputStream(数据流)用来做通信是比较好用的。
/**
* 目标:完成TCP通信快速入门-客户端开发:实现1发1收。
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1、创建Socket对象,并同时请求与服务端程序的连接。
Socket socket = new Socket("127.0.0.1", 8888);
//这里填InetAddress.getLocalHost().getHostAddress()也行 则是自己实际的ip地址
// 2、从socket通信管道中得到一个字节输出流,用来发数据给服务端程序。
OutputStream os = socket.getOutputStream();
// 3、把低级的字节输出流包装成数据输出流
DataOutputStream dos = new DataOutputStream(os);
// 4、开始写数据出去了
dos.writeUTF("在一起,好吗?");
dos.close();//关闭外部包装流也会自动把内部包装的字节流给关闭的
socket.close(); // 释放连接资源
}
}
TCP服务端
把服务端写一下,这里的服务端用来接收客户端发过来的数据。
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("-----服务端启动成功-------");
// 1、创建ServerSocket的对象,同时为服务端注册端口。
ServerSocket serverSocket = new ServerSocket(8888);
// 2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
Socket socket = serverSocket.accept();
// 3、从socket通信管道中得到一个字节输入流。
InputStream is = socket.getInputStream();
// 4、把原始的字节输入流包装成数据输入流
DataInputStream dis = new DataInputStream(is);
// 5、使用数据输入流读取客户端发送过来的消息
String rs = dis.readUTF();
System.out.println(rs);
// 其实我们也可以获取客户端的IP地址
System.out.println(socket.getRemoteSocketAddress());
dis.close();
socket.close();
}
}
TCP通信——多发多收
TCP客户端
/**
* 目标:完成TCP通信快速入门-客户端开发:实现客户端可以反复的发消息出去
*/
public class Client {
public static void main(String[] args) throws Exception {
// 1、创建Socket对象,并同时请求与服务端程序的连接。
Socket socket = new Socket("127.0.0.1", 8888);
// 2、从socket通信管道中得到一个字节输出流,用来发数据给服务端程序。
OutputStream os = socket.getOutputStream();
// 3、把低级的字节输出流包装成数据输出流
DataOutputStream dos = new DataOutputStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请说:");
String msg = sc.nextLine();
// 一旦用户输入了exit,就退出客户端程序
if("exit".equals(msg)){
System.out.println("欢迎您下次光临!退出成功!");
dos.close();
socket.close();
break;
}
// 4、开始写数据出去了
dos.writeUTF(msg);
dos.flush();//一定要调用个flush方法 把数据刷新出去 防止这个数据还在客户端内存中 也即立即发送给服务端的意思
}
}
}
TCP服务端
需要我们注意的时,如果客户端Socket退出之后,就表示连接客户端与服务端的数据通道被关闭了,这时服务端就会出现异常。服务端可以通过出异常来判断客户端下线了,所以可以用try…catch把读取客户端数据的代码套一起来,catch捕获到异常后,打印客户端下线。
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("-----服务端启动成功-------");
// 1、创建ServerSocket的对象,同时为服务端注册端口。
ServerSocket serverSocket = new ServerSocket(8888);
// 2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
Socket socket = serverSocket.accept();
// 3、从socket通信管道中得到一个字节输入流。
InputStream is = socket.getInputStream();
// 4、把原始的字节输入流包装成数据输入流
DataInputStream dis = new DataInputStream(is);
while (true) {
try {
// 5、使用数据输入流读取客户端发送过来的消息
String rs = dis.readUTF();
System.out.println(rs);
} catch (Exception e) {
System.out.println(socket.getRemoteSocketAddress() + "离线了!");//拿客户端ip地址
dis.close();
socket.close();
break;
}
}
}
}
注意上一个案例中我们写的服务端程序只能和一个客户端通信,因为因为服务端现在只有一个主线程,只能处理一个客户端的消息。
如果有多个客户端连接服务端,此时服务端是不支持的。
为了让服务端能够支持多个客户端通信,就需要用到多线程技术。
具体的实现思路如下图所示:每当有一个客户端连接服务端,在服务端这边就为Socket开启一条线程取执行读取数据的操作,来多少个客户端,就有多少条线程。按照这样的设计,服务端就可以支持多个客户端连接了。
首先,我们需要写一个服务端的读取数据的线程类,代码如下
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
DataInputStream dis = new DataInputStream(is);
while (true){
try {
String msg = dis.readUTF();
System.out.println(msg);
} catch (Exception e) {
System.out.println("有人下线了:" + socket.getRemoteSocketAddress());
dis.close();
socket.close();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
接下来,再改写服务端的主程序代码,如下:
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("-----服务端启动成功-------");
// 1、创建ServerSocket的对象,同时为服务端注册端口。
ServerSocket serverSocket = new ServerSocket(8888);
while (true) {
// 2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
Socket socket = serverSocket.accept();
System.out.println("有人上线了:" + socket.getRemoteSocketAddress());
// 3、把这个客户端对应的socket通信管道,交给一个独立的线程负责处理。
new ServerReaderThread(socket).start();
}
}
}
客户端还使用原来的代码即可。先启动服务端主程序再启动多个客户端就完成了让服务端能够支持多个客户端通信。
public class Client {
public static void main(String[] args) throws Exception {
// 1、创建Socket对象,并同时请求与服务端程序的连接。
Socket socket = new Socket("127.0.0.1", 8888);
// 2、从socket通信管道中得到一个字节输出流,用来发数据给服务端程序。
OutputStream os = socket.getOutputStream();
// 3、把低级的字节输出流包装成数据输出流
DataOutputStream dos = new DataOutputStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请说:");
String msg = sc.nextLine();
// 一旦用户输入了exit,就退出客户端程序
if("exit".equals(msg)){
System.out.println("欢迎您下次光临!退出成功!");
dos.close();
socket.close();
break;
}
// 4、开始写数据出去了
dos.writeUTF(msg);
dos.flush();//一定要调用个flush方法 把数据刷新出去 防止这个数据还在客户端内存中 也即立即发送给服务端的意思
}
}
}
我们想把刚才的案例,改进成能够实现群聊的效果,就是一个客户端发的消息,其他的每一个客户端都可以收到。
刚才我们写的多个客户端可以往服务端发现消息,但是客户端和客户端是不能直接通信的。想要实现群聊的效果,我们还是必须要有服务端在中间做中转。 具体实现方案如下图所示:
我们可以在服务端创建一个存储Socket的集合,每当一个客户端连接服务端,就可以把客户端Socket存储起来;当一个客户端给服务端发消息时,再遍历集合通过每个Socket将消息再转发给其他客户端。
下面我们改造服务端代码,由于服务端读取数据是在线程类中完成的,所以我们要修改ServerReaderThread类,添加转发功能,同时服务端的主程序里要给它加一个静态变量用于存储在线socket:
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
DataInputStream dis = new DataInputStream(is);
while (true){
try {
String msg = dis.readUTF();
System.out.println(msg);
// 把这个消息分发给全部客户端进行接收。
sendMsgToAll(msg);
} catch (Exception e) {
System.out.println("有人下线了:" + socket.getRemoteSocketAddress());
Server.onLineSockets.remove(socket);//把离线的客户端从集合中抹掉
dis.close();
socket.close();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void sendMsgToAll(String msg) throws IOException {
// 发送给全部在线的socket管道接收。
for (Socket onLineSocket : Server.onLineSockets) {
OutputStream os = onLineSocket.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
dos.writeUTF(msg);
dos.flush();
}
}
}
public class Server {
public static List<Socket> onLineSockets = new ArrayList<>();//集合用于存储所有在线客户端
public static void main(String[] args) throws Exception {
System.out.println("-----服务端启动成功-------");
// 1、创建ServerSocket的对象,同时为服务端注册端口。
ServerSocket serverSocket = new ServerSocket(8888);
while (true) {
// 2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
Socket socket = serverSocket.accept();
onLineSockets.add(socket);//将客户端加入到在线集合
System.out.println("有人上线了:" + socket.getRemoteSocketAddress());
// 3、把这个客户端对应的socket通信管道,交给一个独立的线程负责处理。
new ServerReaderThread(socket).start();
}
}
}
另外还要注意在当前群聊中,我们的客户端当前除了发消息,还需要随时接受服务端转发过来的消息,因为我们的客户端当前只有一个主线程帮我们不断的发消息,所以得单独定义一个线程出来帮助它不断的接收消息,定义一个客户端的读取数据的线程类ClientReaderThread:
public class ClientReaderThread extends Thread{
private Socket socket;
public ClientReaderThread(Socket socket){
this.socket=socket;
}
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
DataInputStream dis = new DataInputStream(is);
while (true){
try {
String msg = dis.readUTF();
System.out.println(msg);
} catch (Exception e) {
System.out.println("自己下线了:" + socket.getRemoteSocketAddress());
dis.close();
socket.close();
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
客户端主程序:
public class Client {
public static void main(String[] args) throws Exception {
// 1、创建Socket对象,并同时请求与服务端程序的连接。
Socket socket = new Socket("127.0.0.1", 8888);
//创造一个独立的线程,负责随时从socket中接受服务端发来的消息。
new ClientReaderThread(socket).start();
// 2、从socket通信管道中得到一个字节输出流,用来发数据给服务端程序。
OutputStream os = socket.getOutputStream();
// 3、把低级的字节输出流包装成数据输出流
DataOutputStream dos = new DataOutputStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请说:");
String msg = sc.nextLine();
// 一旦用户输入了exit,就退出客户端程序
if("exit".equals(msg)){
System.out.println("欢迎您下次光临!退出成功!");
dos.close();
socket.close();
break;
}
// 4、开始写数据出去了
dos.writeUTF(msg);
dos.flush();//一定要调用个flush方法 把数据刷新出去 防止这个数据还在客户端内存中 也即立即发送给服务端的意思
}
}
}
前面我们所写的代码都是基于CS架构的。网络编程还可以编写BS架构的程序,为了体验一下BS架构通信,这里我们写一个简易版的程序。仅仅只是体验下一,后期还会详细学习BS架构的程序如何编写。
BS架构程序的实现原理,如下图所示:不需要开发客户端程序,此时浏览器就相当于是客户端,此时我们只需要写服务端程序就可以了。
在BS结构的程序中,浏览器和服务器通信是基于HTTP协议来完成的,浏览器给服务端发送数据需要按照HTTP协议规定好的数据格式发给服务端,服务端返回数据时也需要按照HTTP协议规定好的数据格式发给浏览器,只有这样双方才能完成一次数据交互。
客户端程序不需要我们编写(浏览器就是),所以我们只需要写服务端就可以了。
服务端给客户端响应数据的数据格式(HTTP协议规定数据格式)如下图所示:左图是数据格式,右图是示例。
接下来,我们写一个服务端程序按照右图示例的样子,给浏览器返回数据,要求从浏览器中访问服务器并立即让服务器响应一个很简单的网页给浏览器展示。注意:数据是由多行组成的,必须按照规定的格式来写。
服务端程序
先写一个线程类,用于按照HTTP协议的格式返回数据
public class ServerReaderThread extends Thread{
private Socket socket;
public ServerReaderThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
// 立即响应一个网页内容:“黑马程序员”给浏览器展示。
try {
OutputStream os = socket.getOutputStream();
PrintStream ps = new PrintStream(os);//写HTTP协议用打印流输出更方便
ps.println("HTTP/1.1 200 OK");
ps.println("Content-Type:text/html;charset=UTF-8");
//告诉浏览器我们的内容是一个文本网页 使用UTF-8编码
ps.println(); // 必须换行!
ps.println("黑马程序员666");
ps.close();
socket.close();//网页数据写完后这次通信即结束了 应该关闭管道 再请求要重新发起管道连接
} catch (Exception e) {
e.printStackTrace();
}
}
}
再写服务端的主程序
/**
* 目标:完成TCP通信快速入门-服务端开发:要求实现与多个客户端同时通信。
*/
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("-----服务端启动成功-------");
// 1、创建ServerSocket的对象,同时为服务端注册端口。
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
// 2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
Socket socket = serverSocket.accept();
System.out.println("有人上线了:" + socket.getRemoteSocketAddress());
// 3、把这个客户端对应的socket通信管道,交给一个独立的线程负责处理。
new ServerReaderThread(socket).start();
}
}
}
运行服务端,然后在浏览器地址里输入127.0.0.1:8080即可。
服务端主程序用线程池改进
每次请求都开一个线程在高并发时容易宕机,为了避免服务端创建太多的线程,可以把服务端用线程池改进,提高服务端的性能。而且核心线程响应网页数据还是挺快的,一旦响应完之后就空闲出来了又可以去处理下一个管道的任务。
先写一个给浏览器响应数据的线程任务
public class ServerReaderRunnable implements Runnable{
private Socket socket;
public ServerReaderRunnable(Socket socket){
this.socket = socket;
}
@Override
public void run() {
// 立即响应一个网页内容:“黑马程序员”给浏览器展示。
try {
OutputStream os = socket.getOutputStream();
PrintStream ps = new PrintStream(os);
ps.println("HTTP/1.1 200 OK");
ps.println("Content-Type:text/html;charset=UTF-8");
ps.println(); // 必须换行
ps.println("hello world");
ps.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
再改写服务端的主程序,使用ThreadPoolExecutor创建一个线程池,每次接收到一个Socket就往线程池中提交任务就行。
public class Server {
public static void main(String[] args) throws Exception {
System.out.println("-----服务端启动成功-------");
// 1、创建ServerSocket的对象,同时为服务端注册端口。
ServerSocket serverSocket = new ServerSocket(8080);
// 创建出一个线程池,负责处理通信管道的任务。
ThreadPoolExecutor pool = new ThreadPoolExecutor(16 * 2, 16 * 2, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(8) , Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
while (true) {
// 2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
Socket socket = serverSocket.accept();
// 3、把这个客户端对应的socket通信管道,交给一个独立的线程负责处理。
pool.execute(new ServerReaderRunnable(socket));
}
}
}