一 网络编程概述
1.1 网络编程简介
其实,所谓的网络编程,就是编写程序,实现让同一个网络中的机器实现数据的传递,实现通信。
Java是 Internet 的语言,它从语言级上提供了对网络应用程序的支持。
Java提供的网络类库,可以实现无痛的网络连接,联网的底层细节被隐藏在 Java 的本机安装系统里,由 JVM 进行控制,并且Java 实现了一个跨平台的网络库,因此程序员面对的是一个统一的网络编程环境,很容易开发常见的网络应用程序。
1.2 网络编程需要具备的条件
如果要实现两台机器之间实现通信,必须要满足几点要求:
1、需要知道对方的 IP 地址。
2 、需要知道对方的哪一个端口号来做数据的接受。
3 、通信的双方,需要遵循相同的通信协议。
1.3 IP地址和端口号的简介
1.3.1 IP地址
IP是 Internet Protocol (网络互连协议),在计算机中,使用IP地址来描述一个上网终端的唯一的地址编号。分为 IPv4 和 IPv6。
IPv4 : 使用4个字节来描述一个IP地址,由四部分组成,每一部分一个字节。
IPv6 : 使用6个字节来描述一个IP地址,由六部分组成,每一部分一个字节。
IP 地址的分类:
A类: 1.0.0.1 ~ 126.255.255.254 :保留给政府机构
B类: 128.0.0.1 ~ 191.255.255.254 :分配给大中型企业
C类: 192.0.0.1 ~ 223.255.255.254 :分配给任何有需要的个人
D类: 224.0.0.1 ~ 239.255.255.254 :用于组播
E类: 240.0.0.1 ~ 255.255.255.254 :用于实验
1.3.2 端口号的简介
端口是设备与外界进行通信的数据的出口。端口号的范围: [0,65535]。
常见的端口占用:
3306: MySQL
1521 :Oracle
8080 :Tomcat
1.3.3InetAddress
java获取ip的代码演示:
常用API之InetAddress
1. 该类常用于描述一台机器ip和主机名信息
2. 常用方法:
static InetAddress getLocalHost()
static InetAddress getByName(String hostname)
String getHostAddress()
String getHostName()
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
public class _01InetAddressDemo {
public static void main(String[] args) {
try {
// 获取一个描述本机信息的InetAddress对象: 包括主机名称和IP地址
InetAddress localHost = InetAddress.getLocalHost();
System.out.println(localHost);
//获取里面的主机名称
System.out.println("主机名称:"+localHost.getHostName());
//获取里面的IP地址
System.out.println("IP地址:"+localHost.getHostAddress());
//IP的字节序列形式
byte[] address = localHost.getAddress();
System.out.println("IP的字节序列形式:"+ Arrays.toString(address));
// 描述本机的形式有: 127.0.0.1 localhost 具体IP
InetAddress a = InetAddress.getByName("127.0.0.1");
System.out.println(a);
InetAddress inet2 = InetAddress.getByName("www.baidu.com");
System.out.println(inet2.getHostAddress());
System.out.println(inet2.getHostName());
// 获取域名对应的所有主机
InetAddress[] allByName = InetAddress.getAllByName("www.baidu.com");
System.out.println(allByName.length);
for (InetAddress inetAddress : allByName) {
System.out.println(inetAddress);
}
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}
1.4 通信协议
常用的通信协议有:TCP 、UDP 、HTTP;而OSI模型过于理想化,未能在因特网上进行广泛推广,在这里章节,我们着重讲解一下TCP协议。
1.4.1 TCP协议
是一个传输层的通信协议,是一个安全的,面向连接的通信协议。所谓的安全,指的是在数据传递的过程中,不会出现数据丢失的问题。 特点:安全的、面向连接。
TCP/IP 因其传输控制协议(TCP)和网络互联协议(IP)而得名。实际上是一组协议,其包含多个具有不同功能且互为关联的协议。
TCP/IP协议模型从更实用的角度出发,形成了高效的四层体系结构,即网络接口层、IP层、传输层和应用层。下图表示了TCP/IP的分层结构和与OSI参考模型的对应关系:
1.4.2 TCP的三次握手
使用TCP实现数据在两个计算机之间进行传递,需要先在两个计算机之间建立一个安全的连接,然后再 通过这个连接,进行数据的传递。因此, TCP是一个安全的通信协议。
在使用TCP进行数据传递的时候,存在两个角色,客户端和服务端。
在TCP中,连接的建立过程会经历如下几个步骤:
第一次握手
建立连接时,由客户端向服务端发送一个 syn 包, syn包中是同步序列编号, 并进入SYN_SENT 状态,并等待服务器确认。
第二次握手
服务器收到了 syn 包,服务器必须确认客户端的syn,同时自己也发送一个syn包给客户端,即 SYN_ACK 包,并进入到 SYN_RECV 状态。
第三次握手
客户端收到了服务端 SYN_ACK 包,并向服务端回复一个 ACK 包,此时客户端和服务端会进 入到TCP连接成功状态。 ESTABLISHED。
1.4.3 TCP与UDP的区别
运输层协议中有两个非常重要的协议:
-
传输控制协议TCP(Transmission Control Protocol)
-
用户数据报协议UDP(User Datagram Protocol)
TCP是面向连接的运输层协议。即应用进程(或程序)在使用TCP协议之前,必须先建立TCP连接,在传输完毕后,释放已经建立的连接。利用TCP协议进行通信的两个应用进程,一个是服务器进程。另一个是客户进程。
UDP是面向无连接的运输层协议。即应用进程(或程序)在使用UDP协议之前,不必先建立连接。自然,发送数据结束时也没有连接需要释放。因此,减少了开销和发送数据之前的时延。
二 TCP/IP程序编程
2.1 TCP/IP编程简介
2.1.1 C/S架构模型
CS架构由客户端和服务器两个部分组成,通常采用C/S模式进行通信。其中,客户端负责向用户提供界面和交互功能,而服务器则负责存储和处理数据。在这种架构中,客户端和服务器之间通过网络进行通信,客户端向服务器发出请求,服务器响应并返回相应的结果。
目前,CS架构已被广泛应用于各种场景中,如Web应用程序、电子邮件、数据库管理等,并且具有可扩展性、可靠性和安全性等优点。
TCP利用Socket(套接字)接口来实现C/S模型的网络程序开发,其早已被广泛的采用。通常,主动发起通信的应用程序属于客户端。而服务器则是等待通信请求,当服务器收到客户端的请求,执行需要的运算然后向客户端返回结果。
如我们使用QQ软件时,我们电脑上安装的就是一个客户端软件,而腾讯必需运行一个服务器。
2.1.2 Socket套接字介绍
socket被称为套接字,用于描述IP地址和端口号。主机上一般运行了多个服务软件,同时提供多种服务,每种服务都会打开一个Socket,并绑定到一个端口上,不同的端口对应不同的服务。
Socket和ServerSocket类位于java.net包中,ServerSocket用于服务端,Socket是建立网络连接时使用的。在连接成功时,应用程序两端都会产生一个Socket实例,两个Socket构成一个通信管道。操作这个实例,完成所需要的通信。
套接字的基本操作有七个基本操作:
连接到远程主机
绑定到端口
接收从远程机器来的连接请求
监听到达的数据
发送数据
接收数据
关闭连接。
2.1.3 Socket编程步骤
套接字编程分为服务器编程和客户端编程,其通信模型如图所示:
1)服务端编程步骤如下:
-
调用 ServerSocket(int port) 创建一个服务器端套接字,并绑定到指定端口上。
-
调用 accept(),监听连接请求,如果客户端请求连接,则接受连接,返回通信套接字。
-
调用 Socket类的 getOutputStream 和 getInputStream 获取输出流和输入流,开始网络数据的发送和接收。
-
最后关闭通信套接字。
演示代码:
TCP通信协议的服务端API
1.ServerScoket 用来描述TCP的服务端
2. 常用构造器:
ServerSocket(int port): 用于指定一个端口号,返回具体实例
3. 常用方法:
setSoTimeout(int timeout): 用于定义服务端超时时间,单位是毫秒。超过指定时间,抛异常
设置为0时,表示一直等待
accept(): 一个阻塞方法,用于监听客户端的连接,如果监听到,则返回一个Socket对象。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class _02ServerSocketDemo {
public static void main(String[] args) {
//创建一个服务端的实例对象,指定端口号必须是没使用过的。
try {
ServerSocket server = new ServerSocket(9999);
//设置超时时间,单位是毫秒。
// server.setSoTimeout(10000);
//调用accept方法监听客户端的连接: 是否连接服务器
System.out.println("-----等待客户端连接,仅限十秒-----");
Socket socket = server.accept();//如果制定了超时时间,该时间段内没有客户端连接,抛SocketTimeoutException
System.out.println("-----一个客户端已经连接上了-----");
//获取输入流,等待客户端传过来的信息
System.out.println("-----等待客户端传过来的信息-----");
InputStream inputStream = socket.getInputStream();
BufferedReader br = new BufferedReader(
new InputStreamReader(inputStream,"utf-8"));
String s = br.readLine();
System.out.println("服务端收到客户端的信息:"+s);
// System.out.println("服务端IP: "+server.getInetAddress().getHostAddress());
// System.out.println("服务端IP: "+server.getInetAddress().getHostName());
// System.out.println("正在使用的端口号:"+server.getLocalPort());
// System.out.println("详细信息:"+server.getLocalSocketAddress());
// System.out.println("超时时间:"+server.getSoTimeout());
// System.out.println("服务器绑定的端口是否是正确的:"+server.isBound());
// System.out.println("服务端是否关闭:"+server.isClosed());
} catch (IOException e) {
e.printStackTrace();
}
}
}
2)客户段编程步骤如下:
-
创建 Socket。根据指定的 IP和port构造 Socket 类对象,并发送连接请求。如服务器端响应,则建立客户端到服务器的通信线路。
-
通过Socket获取与服务器的通信流。 使用 getInputStream()方法获得输入流,使用 getOutputStream()方法获得输出流。
-
按照一定的协议对 Socket进行读/写操作。通过输入流读取服务端响应的信息,通过输出流将信息发送给服务端。
-
关闭 Socket。断开客户端到服务器的连接,释放线路。
代码演示:
TCP通信协议的客户端API
1.Socket这个类用于描述TCP的客户端
2. 常用构造器
Socket(String host, int port): 指定要连接的服务器地址和端口号
Socket(InetAddress host, int port): 指定要连接的服务器地址和端口号
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
public class _03SocketDemo {
public static void main(String[] args) {
//指定服务器的IP和端口号,返回一个客户端的Socket对象,连接服务器
try {
// Socket socket = new Socket("127.0.0.1", 9999);
Socket socket=new Socket(InetAddress.getByName("192.168.1.120"),9999);
// System.out.println("-----客户端已经连接上了服务器-----");
OutputStream out = socket.getOutputStream();
PrintWriter pw = new PrintWriter(
new OutputStreamWriter(out,"Utf-8"),true);
pw.println("你好,我是客户端,我现在非常开心,就像混凝土里面添加了爱情。");
pw.println("爆发了詹姆斯传球勾股定理");
System.out.println("发送完毕!");
//测试客户端的其他方法
InetAddress address = socket.getInetAddress();
System.out.println("服务器地址"+address);
InetAddress localAddress =socket.getLocalAddress();
System.out.println("客户端地址"+localAddress);
int localPort = socket.getLocalPort();
System.out.println("客户端自己用于通信的端口号(系统分配的):"+localPort);
} catch (IOException e) {
e.printStackTrace();
}
}
}
3)流连接
-
客户端和服务器端的套接字对象诞生以后,必须进行输入、输出流的连接。
-
套接字调用 close()可以关闭双方的套接字连接,只要一方关闭连接,就会导致对方发生IOException异常。
三 聊天小项目
服务端代码部分:
package com.se.day12.chat1;
/*
聊天服务器
*/
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class ChatServer {
//定义一个TCP通信协议的服务端属性
private ServerSocket server;
// 添加一个Map属性,用于存储多个客户端的表示与输出流对象,Map集合(散列表)
private Map<String,PrintWriter> allOut;
public ChatServer(int port){
try {
server = new ServerSocket(port);
// 给map集合赋值
allOut = new HashMap<String,PrintWriter>();
} catch (IOException e) {
System.out.println("----------服务器启动失败-----------");
}
}
public void start(){
try {
while(true){
System.out.println("---等待客户端连接---");
Socket socket = server.accept();
System.out.println("---一个客户端连接成功---");
// 没获取一个客户端的socket对象,就应该将其放入一个并发线程中
getClientInfoHandler getClientInfoHandler = new getClientInfoHandler(socket);
Thread thread = new Thread(getClientInfoHandler);
thread.start();
}
} catch (IOException e) {
System.out.println("---一个客户端连接超时---");
}
}
public static void main(String[] args) {
// 创建一个具体的服务器对象
ChatServer chatServer = new ChatServer(8848);
//调用聊天的主方法
chatServer.start();
}
/*
定义一个处理客户端信息的处理器,即一个任务体
*/
class getClientInfoHandler implements Runnable{
// 因为run方法中使用了start方法中的socket局部变量
// 所以可以在该类中添加一个属性,run方法中可以访问该类的属性
private Socket socket;
// 提供一个构造器,将start方法中的局部变量socket传进来,给属性赋值,这样run方法中使用的socket就是
//start方法中的局部变量指向的对象。
public getClientInfoHandler(Socket socket) {
this.socket = socket;
}
public void run() {
String nickname = null;
try {
//通过服务端的Socket对象获取输入流,读取客户端发送过来的信息
InputStream inputStream = socket.getInputStream();
BufferedReader br = new BufferedReader(
new InputStreamReader(inputStream,"utf-8"));
// 通过服务端的Socket对象,获取输出流,将信息返回给客户端
OutputStream outputStream = socket.getOutputStream();
PrintWriter pw = new PrintWriter(
new OutputStreamWriter(outputStream,"utf-8"),true);
//先获取客户端的昵称
nickname = br.readLine();
//判断昵称是否重复,如果已经被占用,加后缀
while(allOut.containsKey(nickname)){
nickname = nickname+"_"+System.currentTimeMillis();
}
// 将昵称告诉客户端
pw.println(nickname);
// 将输出流添加到Map集合中
allOut.put(nickname,pw);
System.out.println("-------在线人数: " +allOut.size()+"人------");
// 循环打印客户端发送的信息
String info = "";
while((info = br.readLine())!=null){
if (info.startsWith("@")){
//私聊
sendPrivateInfo(nickname,info);
}else {
//将信息info发送到所有的客户端里
sendToAllClient(nickname,info);
}
}
} catch (Exception e) {
System.out.println("---一个客户端离线了---");
// 删除队友的键值对儿
allOut.remove(nickname);
System.out.println("-------在线人数: " +allOut.size()+"------");
}
}
// public void sendPrivateInfo(String nickname,String info){
// String[] split = info.split(":");
// split[0]=split[0].substring(1);
// if(allOut.containsKey(split[0])){
// allOut.get(nickname).println("你悄悄对"+split[0]+"说:"+split[1]);
// allOut.get(split[0]).println(nickname+"悄悄对你说:"+split[1]);
// }else {
// allOut.get(nickname).println("该用户不在线");
// }
//
// }
public void sendPrivateInfo(String nickname,String info){
String s1 = info.substring(1, info.indexOf(":"));
String s2 = info.substring(info.indexOf(":")+1);
if(allOut.containsKey(s1)){
allOut.get(nickname).println("你悄悄对"+s1+"说:"+s2);
allOut.get(s1).println(nickname+"悄悄对你说:"+s2);
}else {
allOut.get(nickname).println("该用户不在线");
}
}
public void sendToAllClient(String nickname,String info){
// 遍历Map里的所有输出流,来发送info信息
Set<String> nicks = allOut.keySet();
for(String nick:nicks){
// 获取对应的输出流,然后发送信息
allOut.get(nick).println(nickname+"说:"+info);
}
}
}
}
客户端代码部分:
package com.se.day12.chat1;
/*
聊天客户端
*/
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
public class ChatClient {
//定义一个TCP通信协议的客户端属性
private Socket socket;
public ChatClient(){
//获取socket对象,同时向服务端发送连接请求,连接失败,Connection refused
try {
socket = new Socket("localhost",8848);
} catch (IOException e) {
System.out.println("---服务器崩溃---");
}
}
//聊天的启动方法
public void start() {
try {
// 获取一个获取服务端信息的处理器任务
Runnable task = new GetServerInfoHandler();
// 创建一个线程对象
Thread thread = new Thread(task);
thread.start();
//获取向服务器发送信息的的输出流对象
OutputStream os = socket.getOutputStream();
// 封装成字符缓存流,可以按行输出
PrintWriter pw = new PrintWriter(
new OutputStreamWriter(os,"Utf-8"),
true);
//使用控制台扫描对象类型,不断地扫描控制台上的文字
Scanner scan = new Scanner(System.in);
//先设置昵称
System.out.println("请输入您的昵称:");
String nickname = scan.nextLine();
// 将昵称发送给服务器,让服务器校验昵称是否已经被占用
pw.println(nickname);
System.out.println("===开始聊天===");
while(true){
String s = scan.nextLine();
//将扫描的信息发送到服务端
pw.println(s);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
//创建一个具体的聊天客户端 对象
ChatClient chatClient = new ChatClient();
//调研客户端的主方法
chatClient.start();
}
/*
编写一个获取服务端信息的处理器。即一个任务体
*/
class GetServerInfoHandler implements Runnable{
@Override
public void run() {
try {
// 获取接收服务端发送过来的数据的输入流对象
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(
new InputStreamReader(is,"utf-8"));
//获取自己的昵称:
String nickname = br.readLine();
System.out.println("您的昵称为:"+nickname);
String line = "";
while((line = br.readLine())!=null){
System.out.println(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}