扉:
- 本作学习视频来源:https://www.bilibili.com/video/BV1Nx411r7Pr?t=940&p=11
- 界面参考: https://blog.csdn.net/fszeng2011/article/details/42743323?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-1
-
Sock定义与方法详解参考
-
Socket详解
-
TCP与UDP区别
-
点击跳转百度网盘
提取码:pgfj -
连接时请注意端口号和查看本机IP
1)cmd ipconfig
2)点击网络查看属性 - TestMySocket为服务器端(Java开发) MySocketClient为安卓端(Kotlin开发)
-
定义
:是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据
一:首先在浏览器输入127.0.0.1
- 若无法访问 ping这个地址看能不能ping通 。若能通的话则看看IIS开了没有
-
开启互联网信息服务
IIS
二: 代码块
1. (创建本地服务器一)最简单的例子
-
建立Java项目
-
核心:建立ServerSocket对象并使用accept()建立连接,建立后来个弹窗
1)建立服务端的socket
2)接收socket对象建立连接 搞定
public class MyServerSocket {
public static void main(String[] args){
System.out.println("Hello, World!");
//port 1-65535
try{
ServerSocket serverSocket=new ServerSocket(12345);
//accept是个阻塞的方法 会阻塞当前线程 返回socket类型
Socket socket=serverSocket.accept();
//建立连接
JOptionPane.showMessageDialog(null,"有客户机连接到了本机的12345端口");
}catch (IOException e){
e.printStackTrace();
}
}
}
Kotlin写法
object MyServerSocket {
fun main(args: Array<String>) {
println("Hello, World!")
//port 1-65535
try {
val serverSocket = ServerSocket(12345)
//accept是个阻塞的方法 会阻塞当前线程 返回socket类型
val socket = serverSocket.accept()//接收客户端请求
//建立连接
JOptionPane.showMessageDialog(null, "有客户机连接到了本机的12345端口")
} catch (e: IOException) {
e.printStackTrace()
}
}
}
-
点击运行后,访问127.0.0.1:12345 是没有界面的,但可以看到。关闭窗体后 程序自动关闭
2. (创建本地服务器二) 解决线程阻塞问题,完成服务器向可客户端发送数据的功能
-
建立ServerListener类继承Thread
1)复写run方法
2)其中循环监听,循环返回新接入的socket对象
3)每当有新的accept进入接起连接,就再次 new一个线程进行通话管理
public class ServerListener extends Thread {
@Override
public void run() {
System.out.println("Hello, World!");
//port 1-65535
try{
ServerSocket serverSocket=new ServerSocket(12345);
//accept是个阻塞的方法 会阻塞当前线程 返回socket类型
//每当有客户端连接 accept都会返回一个socket对象 循环接听客户端的连接
while(true){
Socket socket=serverSocket.accept();
//建立连接
JOptionPane.showMessageDialog(null,"有客户机连接到了本机的12345端口");
//每个socket又要与每个独立的客户端进行通信 所以要将socket传递给新的线程
new ChatSocket(socket).start();//循环监听客户端请求
}
}catch (
IOException e){
e.printStackTrace();
}
}
}
-
新建ChatSocker用于单独处理每个socket
1)Linux 不区分套接字文件和普通文件,使用 write() 可以向套接字中写入数据,使用 read() 可以从套接字中读取数据。两台计算机之间的通信相当于两个套接字之间的通信,在服务器端用 write() 向套接字写入数据,客户端就能收到,然后再使用 read() 从套接字中读取出来,就完成了一次通信。
public class ChatSocket extends Thread {
Socket socket;
public ChatSocket(Socket s){
this.socket=s;
}
//写回方法
public void out(String out) throws IOException {
socket.getOutputStream().write(out.getBytes("UTF-8"));
}
@Override
public void run() {
int count=0;
while (true){
count++;
try {
out("loop"+count);
sleep(1000);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
}
1)运行多个窗体访问
2)
telnet localhost 12345
3)多个线程不同,每个分配独立的chatsocket线程,所以比如要管理线程
3. (创建本地服务器完成)让多个终端同步显示,建立管理线程类
- 测试发现 左上角有特化图标的输入不会显示出来字,但可以发送 类似保密
-
而非特化的输入可以看到也可以发送。
1)private私有化构造方法
2)建立单例返回cm对象
3)使用Vector报错每个线程对象
4)使用方法publish发送信息,for循环向每个发送(输出流,不存缓冲区直接显示的)
代码一:建立管理类
5)补充Kotlin的Vector写法
public class ChatManager {
//私有化构造方法 单例化
private ChatManager() { }
private static final ChatManager cm = new ChatManager();
public static ChatManager getChatManaget() {
return cm;
}
Vector<ChatSocket> vector = new Vector<ChatSocket>();
public void add(ChatSocket cs) {
vector.add(cs);
}
//某个线程向所有客户端发送信息
public void publish(ChatSocket cs, String out) throws IOException {
System.out.print(out);
for (int i = 0; i < vector.size(); i++) {
System.out.println("当前cs"+cs);
ChatSocket csChatSoccket = vector.get(i);
if (!cs.equals(csChatSoccket)){
String msg = "";
msg += out + "\n";//加个换行 不然安卓收不到 太坑了
csChatSoccket.out(msg);
}
}
}
}
代码二:建立了解后单独保存并且封入管理类
public class ServerListener extends Thread {
@Override
public void run() {
System.out.println("Hello, World!");
//port 1-65535
try{
ServerSocket serverSocket=new ServerSocket(12345);
//accept是个阻塞的方法 会阻塞当前线程 返回socket类型
//每当有客户端连接 accept都会返回一个socket对象 循环接听客户端的连接
while(true){
Socket socket=serverSocket.accept();
//建立连接
JOptionPane.showMessageDialog(null,"有客户机连接到了本机的12345端口");
//每个socket又要与每个独立的客户端进行通信 所以要将socket传递给新的线程
/*更新1.
匿名方式直接创建 改为命名方式(赋个值 然后可以被调用)
new ChatSocket(socket).start();//循环监听客户端请求*/
ChatSocket cs=new ChatSocket(socket);
cs.start();
ChatManager.getChatManaget().add(cs);
}
}catch (
IOException e){
e.printStackTrace();
}
}
}
代码三:读入缓冲区的内容并发送消息给终端
public class ChatSocket extends Thread {
Socket socket;
public ChatSocket(Socket s){
this.socket=s;
}
public void out(String out) throws IOException {
socket.getOutputStream().write(out.getBytes("UTF-8"));
}
@Override
public void run() {
// int count=0;
/*
* 修改2. 服务器循环读取消息再发送给终端
* */
try {
BufferedReader br=new BufferedReader(
new InputStreamReader(
socket.getInputStream(),"UTF-8"));
String line=null;
while ((line=br.readLine())!=null) {
ChatManager.getChatManaget().publish(this,line);
}
br.close();
} catch (IOException e) {
e.printStackTrace();
}
//count++;
/*try {
out("loop"+count);
sleep(1000);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}*/
}
}
关闭一个窗口后后,若未remove数组中的东西 ,则会报错(1. socked
关闭报错 2.读写流关闭报错(安卓))。每次连接的这个弹窗提示要点掉,不然会阻塞线程。
4. 安卓代码
- 给权限
<uses-permission android:name="android.permission.INTERNET" />
- AsyncTask防止弃用修改版本
compileSdkVersion 29
buildToolsVersion "29.0.2"
class MainActivity : AppCompatActivity(), View.OnClickListener {
companion object {
const val KEY = "key"
const val TAG = "My Phone"
}
//创建一个socket
private var socket: Socket? = null
//创建输入输出流
private var writer: BufferedWriter? = null
private var reader: BufferedReader? = null
private var socketAddress: SocketAddress? = null
private var isConnected = false
private var sp: SharedPreferences? = null
private var currentName: String? = null
private var connectFailed = false
private var progressDialog: ProgressDialog? = null
private var task: AsyncTask<Void?, String?, Void?>? = null
val currentPosition by lazy { StringBuilder() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
init()
}
private fun init() {
btnConnect.setOnClickListener(this)
btnSend.setOnClickListener(this)
btnSetName.setOnClickListener(this)
tvConnectState!!.text = "连接状态:未连接"
//初始化sharepreference
sp = getPreferences(MODE_PRIVATE)
sp?.let {
currentName = it.getString(MainActivity.Companion.KEY, null)
}
if (currentName != null) {
tvCurrentName!!.text = "当前昵称:$currentName"
}
}
override fun onClick(v: View) {
when (v.id) {
R.id.btnConnect ->
if (currentName != null) {
connect()
} else {
Toast.makeText(this, "请先设置昵称后重新连接", Toast.LENGTH_SHORT).show()
}
R.id.btnSend -> send()
R.id.btnSetName -> {
val tmp = etSetName!!.text.toString().trim { it <= ' ' }
setName(tmp)
}
}
}
// ==================================== 逻辑相关
private fun setName(s: String) {
if (s != "") {
val editor = sp!!.edit()
editor.putString(MainActivity.Companion.KEY, s)
if (editor.commit()) {
tvCurrentName!!.text = "当前昵称:$s"
currentName = s
etSetName!!.setText("")
Toast.makeText(this, "昵称设置成功", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "昵称设置失败,请重试", Toast.LENGTH_SHORT).show()
}
} else {
Toast.makeText(this, "昵称不能为空", Toast.LENGTH_SHORT).show()
}
}
private fun connect() {
progressDialog = ProgressDialog.show(this," 连接状况", "正在连接...")
var read: AsyncTask<Void, String, Void> = @SuppressLint("StaticFieldLeak")
object : AsyncTask<Void, String, Void>() {
@SuppressLint("WrongThread")
override fun doInBackground(vararg params: Void?): Void? {
//把这些放Asynctask中更好 不允许在主线程连接
val ip = etIP!!.text.toString()
val port = Integer.valueOf(etPort!!.text.toString())
//socket = java.net.Socket(ip, port)//Socket(etIP!!.text,)
//设置最长连接时间
socket= Socket()
socketAddress = InetSocketAddress(ip, port)
socket?.let {
it.connect(socketAddress,3000)
//包装Buffererd
writer = BufferedWriter(OutputStreamWriter(it.getOutputStream()))
reader = BufferedReader(InputStreamReader(it.getInputStream()))
//连接成功就发送这个
publishProgress("@success")
}
var line: String? = null
Log.e(TAG, "doInBackground: Start$line")
//就是line不为null
while (reader!!.readLine().also({ line = it }) != null) {
Log.e(TAG, "doInBackground: 进入循环$line" )
publishProgress(line)
}
Log.e(TAG, "doInBackground: 退出循环$line" )
return null
}
override fun onProgressUpdate(vararg values: String?) {
//这里没做连接失败的反馈 相加自己加个glag
if (values[0].equals("@success")){
tvConnectState!!.text = "连接状态:已连接"
Toast.makeText(this@MainActivity, "连接成功", Toast.LENGTH_SHORT).show()
progressDialog!!.dismiss()
}
//发布values的第一个对象
Log.e(TAG, "onProgressUpdate: Start$values[0]" )
currentPosition.append("别人说:==${values[0]}"+"\n")
tvChatRecord?.text=currentPosition
super.onProgressUpdate(*values)
}
}
read.execute()
}
//不涉及联网操作 放主线程应该没问题 有问题 flush
private fun send() {
Thread(Runnable {
//一定要追加换行符
currentPosition.append("我说:${etMassage.text}").append("\n")
writer?.write(etMassage.text.toString() + "\n")
writer?.flush()
runOnUiThread {
etMassage!!.setText("")
tvChatRecord?.text = currentPosition
}
//数据强制输出 安卓反复开启 服务器会有混淆 报closed错误
}).start()
}
override fun onDestroy() {
writer?.close()
reader?.close()
super.onDestroy()
}
}
5.调试过程和同步更新机制
-
第一个阶段,完成连接发送更新UI消息,之后便进入了While循环,从断点我们知道一进入就不会退出循环了(哪怕刚开始取到空,
只是单纯的 不执行代码
这种类似以起c++ !=EOF输入CTRL+Z才退出循环,一直在循环中 发现输入流有东西(其他端实现)就直接通知UI更新)
-
实现我发的就是我说,其他人发的就其他人说:这个比较简直自己发的直接获取为我说,AsyncTask中接到的所有消息都是他说
-
发送消息非常重要加换行符,估计是侦测借位换行符的,亲测结尾加上两个换行符也达不到换行效果。
-
打断点暂时测不出来关闭连接后的 情况
6. 补充AsyncTask 30弃用之前