Socket
在我的上一篇博客中详细的讲到了Swing的使用与见解,这里着重写一下对Socket的认识与使用。
在大学的计算机网络课程里,我第一次了解到socket(套接字),当时只把它当作一个简单的运输层概念随便记了一下,后面才发现Socket在java的通信中是一个多么重要的概念,现在想想也是,socket在TCP连接的两端,是IP加上端口号的组合,怎么着也和通信脱不了干系啊。但是准确的讲的话,在本文中socket有两层意思。
一个就是上面讲的TCP连接的两端,在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。
再一个就是java中的socket类了。这是通过代码进行socket编程,通讯软件都离不开socket的帮忙。下面随便记点java中的socket函数
Class ServerSocket (服务器)
这个类实现了服务器套接字。 服务器套接字等待通过网络进入的请求。 它根据该请求执行一些操作,然后可能将结果返回给请求者。
ServerSocket(int port)
创建绑定到指定端口的服务器套接字。这是我目前最常用的一种构造方法,只需要传入一个本地的端口浩就行了(1024以下端口由系统使用,建议使用较高数值端口)
accept()
侦听要连接到此套接字并接受它。创建好socket后肯定是等待用户连接进来了,accept函数返回的是已连接的socket描述字。当有多个socket连接进服务器的时候,一般要使用多线程。(采用while( true)死循环一直监听连接请求)每个socket连接线程在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
close()
关闭此套接字。
getInetAddress()
返回此服务器套接字的本地地址。
isBound()
返回ServerSocket的绑定状态。
isClosed()
返回ServerSocket的关闭状态。
Socket(客户端)
该类实现客户端套接字(也称为“套接字”)。 套接字是两台机器之间通讯的端点。
Socket()
创建一个未连接的套接字,并使用系统默认类型的SocketImpl。
Socket(InetAddress address, int port)
创建流套接字并将其连接到指定IP地址的指定端口号。 (常用的创建socket方法)
getInetAddress()
返回套接字所连接的地址。
getLocalAddress()
获取套接字所绑定的本地地址。
getLocalPort()
返回此套接字绑定到的本地端口号。
getInputStream()
返回此套接字的输入流。
InputStreamReader in = new InputStreamReader(socket.getInputStream(),“utf-8”) 将字节流转为字符流,并指定字符集
BufferedReader br = new BufferedReader(in) 读取字符流,为字符流创建缓冲区
getOutputStream()
返回此套接字的输出流。
OutputStreamWriter out = new OutputStreamWriter(socket.getOutputStream(),“utf-8”) 将字节流转为字符流,并指定字符集
BufferedWriter wr = new BufferedWriter(out) 为字符流创建缓冲区
PrintWriter pw = new PrintWriter(wr,true) 打印字符流 并自动刷新
socket工作流程
socket是**open—write/read—close”模式 (Unix/Linux基本哲学之一就是一切皆文件)**的一种实现
我们知道TCP连接需要进行三次握手,那么和socket连接时交互的几步又怎样的联系呢?
当客户端调用connect时(如果时创建socket时就指明了服务器的ip及端口,就不用调用connection了,默认创建后就connection了),触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。
聊天小程序的实现
服务器代码
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
public class Server {
private List<Socket> ClientList= new ArrayList<>();
private ServerSocket serverSocket = null;
SocketThread socketThread;
public static void main(String[] args) {
new Server();
}
public Server(){
try {
serverSocket = new ServerSocket(8887);
System.out.println("服务器已启动,等待用户接入");
while (true){
Socket socket = serverSocket.accept();
ClientList.add(socket);
socketThread = new SocketThread(socket,ClientList);
/**这里的ClientList会实时的传到每个连接每个socket的线程里,
* 比如第一个socket连接进来的时候 ClientList.size为1;
* 第二个socket连接进来,重新开启一个线程,ClientList会重新传值进连接第一个socket的线程
*/
socketThread.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
多线程,每来一个socket就创建一个线程,上文中注释很重要
import java.io.*;
import java.net.Socket;
import java.util.List;
public class SocketThread extends Thread {
private Socket socket;
private List<Socket> clientList;
private BufferedReader br = null;
private String message = "";
public SocketThread(Socket socket, List<Socket> clientList) {
this.socket = socket;
this.clientList = clientList;
try {
br = new BufferedReader(new InputStreamReader(
socket.getInputStream(), "UTF-8"));
message = "服务器地址" + this.socket.getInetAddress();
this.sendMessage(message);
message = "连接总数" + clientList.size();
this.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
super.run();
while (true) {
try {
if ((message = br.readLine()) != null) {
if (message.equals("exit")) {
closeSocket();
break;
} else {
message = socket.getInetAddress() + ":" + message;
this.sendMessage(message);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 本方法关闭socket连接,节约资源
*
* @throws IOException
*/
public void closeSocket() throws IOException {
clientList.remove(socket);
br.close();
message = "主机:" + socket.getInetAddress() + "关闭连接\n目前在线" + clientList.size();
socket.close();
this.sendMessage(message);
}
/**
* 本方法是将信息发送到连接的每一个socket上
* @param msg
* @throws IOException
*/
public void sendMessage(String msg) throws IOException {
System.out.println(msg);
int count = clientList.size();
System.out.println("本线程连接socket数===" + count);
for (int i = 0; i < count; i++) {
Socket mSocket = clientList.get(i);
PrintWriter out = null;
out = new PrintWriter(new BufferedWriter(
new OutputStreamWriter(mSocket.getOutputStream(), "UTF-8")), true);
out.println(msg);
}
}
}
接下来是客户端代码,由于涉及到可视化界面(swing在上篇博客中有详解),代码有点多,但是不复杂
import javax.swing.*;
import java.awt.event.*;
import java.io.*;
import java.net.Socket;
public class Client implements ActionListener, WindowListener, KeyListener {
private Socket socket;
private static final String HOST ="10.22.35.22";
private static final int PORT=8887;
BufferedReader br;
PrintWriter out;
OutputStream os;
OutputStreamWriter osw;
BufferedWriter bw;
JFrame jFrame;
JPanel jPanel;
JTextArea jTextArea;
JTextField jTextField;
JScrollPane jScrollPane;
JButton jButton;
String info;
public static void main(String[] args) throws IOException {
Client client = new Client();
client.showWindow();
client.connection();
}
public void showWindow(){
jFrame = new JFrame("Let'chat");
jPanel = new JPanel();
jTextArea = new JTextArea();
jScrollPane = new JScrollPane(jTextArea);
jScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
jScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
jTextField = new JTextField();
jButton = new JButton("发送");
jScrollPane.setBounds(0,0,500,400);
jTextField.setBounds(0,400,400,100);
jButton.setBounds(400,400,100,100);
jPanel.setLayout(null);
jPanel.setBounds(0,0,510,510);
jPanel.add(jScrollPane);
jPanel.add(jTextField);
jPanel.add(jButton);
jFrame.setSize(540,540);
jFrame.add(jPanel);
jFrame.setLocationRelativeTo(null);
jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jFrame.setVisible(true);
jButton.addActionListener(this);
jFrame.addWindowListener(this);
jTextField.addKeyListener(this);
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource()==jButton){
System.out.println("OK");
out();
}
}
@Override
public void windowOpened(WindowEvent e) {
}
@Override
public void windowClosing(WindowEvent e) {
try {
os = socket.getOutputStream();
osw = new OutputStreamWriter(os,"UTF-8");
bw = new BufferedWriter(osw);
out = new PrintWriter(bw,true);
out.println("exit");
} catch (IOException e1) {
e1.printStackTrace();
}
}
@Override
public void windowClosed(WindowEvent e) {
}
@Override
public void windowIconified(WindowEvent e) {
}
@Override
public void windowDeiconified(WindowEvent e) {
}
@Override
public void windowActivated(WindowEvent e) {
}
@Override
public void windowDeactivated(WindowEvent e) {
}
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
if(e.getKeyCode()==KeyEvent.VK_ENTER){
out();
}
}
@Override
public void keyReleased(KeyEvent e) {
}
public void out()
{
info=jTextField.getText().toString();
try {
os = socket.getOutputStream();
osw = new OutputStreamWriter(os,"UTF-8");
bw = new BufferedWriter(osw);
out = new PrintWriter(bw,true);
out.println(info);
jTextField.setText("");
} catch (IOException e1) {
e1.printStackTrace();
}
}
public void connection() throws IOException {
try {
socket = new Socket(HOST,PORT);
br = new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8"));
out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(),"UTF-8")),true);
} catch (IOException e) {
e.printStackTrace();
System.out.println("连接失败"+e.getMessage());
}
while (true){
if (!socket.isClosed()){
if (socket.isConnected()){
if (!socket.isInputShutdown()){
String getLine;
if ((getLine = br.readLine())!=null){
jTextArea.append(getLine+"\n");
System.out.println(getLine);
}
}
}
}
}
}
}
程序运行截图
由于在一台电脑上操作,显示IP是一样的
注意:要想顺利运行程序,服务器与客户端须在同一局域网,且关闭了防火墙。