1.进程复习
//通过进程去打开应用程序
Process.Start("calc");
Process.Start("mspaint");
Process.Start("notepad");
Process.Start("iexplore", "http://www.baidu.com");
//通过进程去打开指定的文件
ProcessStartInfo psi = new ProcessStartInfo(@"C:\Users\SJD\Desktop\AE.txt");
Process p = new Process();
p.StartInfo = psi;
p.Start();
//进程和线程的关系?一个进程包含多个线程
单线程容易照成程序假死
//前台 后台
Thread
Thread th=new Thread(Test);创建线程
th.IsBcakground=true; 标记它为后台线程
start()启动线程(告诉CPU 我已经准备好了可以被执行,但是具体执行时间,由CPU决定)
abort()终止线程 (终止完成之后不能再用start())
thread.sleep(1)静态方法 可以让当前线程停止一段时间运行
//线程中如何访问控件
程序不允许跨线程访问,会抛异常。
想要不抛异常,在程序加载的时候,取消跨线程的检查
虽然还是不允许这么做,但不让它去检查,它就不知道这个事
control.checkforillegalcrossthreadcalls=false;
//线程里的方法传参
如果线程执行的方法需要参数,那么要求这个参数必须是object类型。
并且参数是传到调用的start()里面
2.socket
电脑,程序之间的电话机 收发数据
协议 两个不同地方的人用普通话通讯
电脑和电脑之间通讯默认的语言
常用协议 UDP和TCP
socket 孔,插座 作为进程通信机制,也称为“套接字” 用来描述IP地址和端口
是一个通信链的句柄(其实就是两个程序通信用的)
IP是连接服务器,端口号是连接想连接的应用程序
在服务端必须要有一个负责监听的socket,看有没有一个客服端连接到我们的服务器
负责监听的socket 创建一个负责通信的socket
3.tcp和udp协议
客服端要连接服务器,首先要发送一个请求过去
要知道服务器的ip地址,知道服务器的应用程序的端口号
就可以准确的连接到服务器的应用程序
TCP比UDP安全稳定,一般不会发生数据丢失
TCP 3次握手 服务器(必须有) 请求(一定是客服端发给服务器)
请求 客户端--->服务器 你有空吗?
服务器--->客户端 我有空
客户端--->服务端 我知道你有空了
三次握手 三次握手成功了,客服端才和服务器相互的收发数据,否则不会进行数据的沟通
安全,稳定,效率相对低一些
UDP 快速 效率高,但不稳定,容易放生数据丢失
客服端给服务器发消息,不管服务器有空没空就发,消息全发过去,具体服务器有没有精力接受这些消息,它不管,反正发过去了。
视频传输用UDP(视频聊天)聊天不清晰但流畅,不要画面清晰却一卡一卡的
3.创建和客户端通信的socket
private void btnStart_Click(object sender, EventArgs e)
{
//当点击开始监听的时候 在服务器端创建一个负责监IP地址跟端口号的Socket
Socket socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress ip = IPAddress.Any;//IPAddress.Parse(txtServer.Text);
//创建端口号对象
IPEndPoint point = new IPEndPoint(ip, Convert.ToInt32(txtPort.Text));
//监听
socketWatch.Bind(point);
ShowMsg("监听成功");
**设置监听队列
设置服务器的最大监听量
//一个时间点10个人,来11个人就1个人排队
socketWatch.Listen(10);
Thread th = new Thread(Listen);
th.IsBackground = true;
th.Start(socketWatch);
}
/// <summary>
/// 等待客户端的连接 并且创建与之通信的socket
/// </summary>
void Listen(object o)
{
Socket socketWatch = o as Socket;
//等待客户端的连接 并且创建一个负责通信的socket
while (true)
{
Socket socketSend = socketWatch.Accept();
//192.168.1.50:连接成功
ShowMsg(socketSend.RemoteEndPoint.ToString() + ":" + "连接成功");
}
}
void ShowMsg(string str)
{
txtLog.AppendText(str + "\r\n");
}
private void Form1_Load(object sender, EventArgs e)
{
Control.CheckForIllegal
CrossThreadCalls = false;
}
4.设计协议
实现传送文件
如何判断接收数据是文件还是文字
设计“协议”:
把要传递的字节数组前面都加上一个字节做为标示。0:标示文字。1:标示文件。
即:文字:0+文字(字节数组标示)
文件:1+文件的二进制信息
将泛型集合转换为数组
byte[] buffer = System.Text.Encoding.UTF8.GetBytes(str);
List<byte> list = new List<byte>();
list.Add(0);
list.AddRange(buffer);
byte[] newBuffer= list.ToArray();
5.创建服务端和客户端通信的流程笔记
客户端 服务端
Socket() Socket()
Bind() 绑定监听端口
Listen() 设置监听队列
while(true)
{
Connect() 连接建立 Accept() 循环等待客户端连接
}
Send() 发送数据 Receive() 循环接收客户端信息
Receive() 接收数据 Send()
Close() 发送数据 捕捉异常 Close()
1.创建服务端的大概界面
第一排 txtServer(输入IP) txtPort(输入端口) btnStart(开始监听) cboUsers(存储连接到的IP和端口,combobox控件)
第二排 txtLog(显示通信结果)
第三排 txtMsg(输入通信信息)
第四排 txtPath(存储和显示传输的文件及路径) btnSelect(选择) btnSendFile(发送文件)
第五排 btnSend(发送消息) btnZD(震动)
2.进入btnStart 入口(点击开始监听执行)
当点击开始监听时 在服务端创建一个负责监听IP和端口号的socket
Socket socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress ip = IPAddress.Any;//IPAddress.Parse(txtServer.Text);
创建端口对象 把IP和端口号拼接起来
IPEndPoint point = new IPEndPoint(ip, Convert.ToInt32(txtPort.Text));
监听
socketWatch.Bind(point);
3.创建一个方法,提示监听成功,并赋值给txtLog.Text.
方法要传入一个string类型的参数,表示监听成功
并且text要使用追加模式,才不会每次传输都覆盖掉。
文本后面+\r\n 换行
void ShowMsg(string str)
{
txtLog.AppendText(str + "\r\n");
}
4.使用ShowMsg方法,传输"监听成功"
ShowMsg("监听成功");
5.继续在btnStart
*ShowMsg("监听成功");
*设置监听队列 只能连进来10个
socketWatch.Listen(10);
*等待客户端连接 并且创建一个负责通信的socket
Socket socketSend= socketWatch.Accept(); socketWatch.Accept就是连接到的客户端通信
*RemoteEndPoint能够拿到远程连过来的IP和端口 输出到txtLog
ShowMsg(socketSend.RemoteEndPoint.ToString()+":"+"连接成功");
6.出现两个问题
1.程序出现假死问题,因为只有一个主线程运行
开线程解决
2.因为接收通信只接收了一次,所以只有一个客户端可以连过来
写在一个循环中解决
7.创建一个方法 等待客户端的连接,并创建与之通信用的socket
void Listen()
{
把第5步的这两行剪切过来
**socketWatch只干一件事情,监听完后接受
Socket socketSend= socketWatch.Accept();
ShowMsg(socketSend.RemoteEndPoint.ToString()+":"+"连接成功");
}
外面加上while(true)
{
}
出现问题 负责监听的socketWatch访问不到了
方法有2 1.第2步的socketWatch声明在外面
2.把socketWatch传进来
跨线程传参只能传object类型
采用第二种
传入一个声明的object类型的参数 o
void Listen(object o)
把object类型的o转为Socket socketWatch
Socket socketWatch = o as Socket;
8. Listen这个函数要被新线程执行,才不会假死
在btnStart方法中创建新线程 把Listen方法放进去
Thread th = new Thread(Listen);
设置为后台线程
th.IsBackground = true;
运行并传入socketWatch给Listen函数
th.Start(socketWatch);
9.程序会报错,在程序加载时,取消跨线程的报错
private void Form1_Load(object sender, EventArgs e)
{
Control.CheckForIllegalCrossThreadCalls = false;
}
用telnet测试一下连接情况
10.在7步中的while循环继续写
客户端连接成功后,服务器应该接收客户端发来的消息
通过负责通信的socket来处理
**跟客户端收发数据都用负责通信的socket socketSend
receive这个方法接受客户端发来的消息,接受字节数组
数据拿过来后先放到一个字节数组里面
byte[] buffer = new byte[1024 * 1024 * 2];
r是实际接受到的有效字节数
int r=socketSend.Receive(buffer);
把传过来的byte[]转换成能读懂的字符串
string str= Encoding.UTF8.GetString(buffer, 0, r);
输出到txtLog文本框里面 调用之前的方法ShowMsg
ShowMsg用来作为输出文本框的方法(自动换行)
socketSend.RemoteEndPoint表示与客户端的IP和端口号
.tostring表示转换为string类型了。和后面的一起作为一个整体的string类型传参给ShowMsg();
在txtLog上显示客户端发送的数据
ShowMsg(socketSend.RemoteEndPoint.ToString() + ":" + str);
11.测试后,发现只能传输一次数字。所以需要把传输写入循环并封装起来。
封装成一个接收客户端信息的方法
void Recive()
{
while (true)
{
byte[] buffer = new byte[1024 * 1024 * 2];
//实际接受到的有效字节数
int r = socketSend.Receive(buffer);
//把buffer转换成能读懂的字符串
string str = Encoding.UTF8.GetString(buffer, 0, r);
ShowMsg(socketSend.RemoteEndPoint.ToString() + ":" + str);
}
}
socketSend读取有问题,也需要传参 同object类型
12.返回Listen的while循环中
开启一个新线程 不停的接收客户端发送过来的消息
Thread th = new Thread(Recive);
th.IsBackground = true;
th.Start(socketSend);
问题1.现在是发一个字符换一行 但要一行显示(这个只要用自己写的客户端做就是正常的了,跳过)
问题2.关了客户端,服务端还是一直在发送。
因为客户端关闭了,r的实际信息是空(也就是0),但还是一直在发送,所以要做一个判断
if(r==0)
{ break; }
把容易发生异常的地方都try-catch起来。catch里面什么都不写
13.创建客户端模块
txtServer(192.168.1.42) txtPort(50000) btnStart(连接)
txtLog
txtMsg
14.点击连接进入btnSatrt_Click
创建负责通信的socket interNetwork表示IPV4 sockettype(类型)流式的 protocoltype(服务)Tcp
Socket socketSend = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
客户端要去连接服务器
首先要用静态方法创建ipAddress 的IP地址
IPAddress ip = IPAddress.Parse(txtServer.Text);
再把IP和端口号拼接一起
IPEndPoint point = new IPEndPoint( ip, Convert.ToInt32(txtPort.Text));
获得要远程连接的服务器的IP和端口号
socketSend.Connect(point);
15.创建一个方法 ShowMsg()
void ShowMsg(string str)
{
要使用追加文本的AppendText才不会被覆盖掉
txtLog.AppendText(str + "\r\n");
}
在14步中传入个"连接成功",表示客户端与服务端连接上了
ShowMsg("连接成功");
16.连接上服务端 客户端就给服务端发送消息
需要再txtMsg写内容 发送给服务器
写在发送消息事件里,进入btnSend_Click
用负责通信的socketSend来通信。但是它写在另一个事件里拿不到,所以把它声明到外面去
socketSend.send()发信息到服务端,需要放入一个字节数组
我们要发送的txtMsg.text里面的信息,接收到string str里面,trim是移除空格
string str = txtMsg.Text.Trim();
再把str转换为字节数组
byte[] buffer = Encoding.UTF8.GetBytes(str);
发送信息到服务端
socketSend.Send(buffer);
17.服务端向客户端发送消息
写在发送消息事件内 btnSend_Click
先将第7步 socket socketSend声明到外面
如同第16步 把txtMsg的数据传到客户端
string str = txtMsg.Text();
byte[] buffer = Encoding.UTF8.GetBytes(str);
socketSend.Send(buffer);
18.客户端 发消息不只一次,要不停的发,所以还要一直接收
写一个和服务端的Recive方法一样。使用wile(true)让他循环使用
void Recive()
{
while(true)
{
}
}
用socketSend.Receive接收字节数组buffer,返回一个实际接受的有效字节数r
byte[] buffer = new byte[1024 * 1024 * 3];
int r= socketSend.Receive(buffer);
如12步,再加上判断字节数为空的时候跳出循环 if(r==0)
if(r==0)
{
break;
}
再将信息的数组解码为字符串s来接收
string str=Encoding.UTF8.GetString(buffer, 0, r);
调用ShowMsg方法 把IP和端口号用:和接收的信息连接起来并换行输出,显示在txtMsg上
RemoteEndPoint 获取IP地址和端口号
ShowMsg(socketSend.RemoteEndPoint.ToString() + ":" + s);
19.返回btnStart_Click事件
防止运行单线程时候卡死,需要开启一个新线程,把Recive的方法传进去,不停的接受服务端发来的消息
Thread th = new Thread(Recive);
设置为后台线程
th.IsBackground = true;
th.Start();
20.为了防止跨线程报错出异常
在form1_load载入时关闭跨线程的检查
Control.CheckForIllegalCrossThreadCalls = false;
凡是涉及网络连接的都try-catch一下
21.现在只能和最后一个连上的客户端通信,因为新的客户端连过来,原来的就没有了。
所以服务端需要一个来存储远程IP和端口的下拉框,给指定的客户端发送消息 cboUsers
用一个键值对来存储 Directory 根据string类型的IP地址去找socketSend 键值对
Dictionary<string, Socket> dicSocket = new Dictionary<string, Socket>();
22.在Listen的socketSend下面添加集合
将远程连接的客户端的IP地址和Socket存入集合中
所以发消息的时候不能再直接用socketSend了,根据选中的IP地址来发送
dicSocket.d(socketSend.RemoteEndPoint.ToString(), socketSend);
将远程连接的IP地址和端口号存储到cboUsers下拉框中
cboUsers.Items.Add(socketSend.RemoteEndPoint.ToString());
转到 btnSend_Click IP和端口
获得用户在下拉框中选中的IP地址指定给string类型的ip
string ip = cboUsers.SelectedItem.ToString();
dicSocket[ip].Send(buffer);
测试
23.现在已经实现了服务端和客户端互相收发数据,但仅限文本类型数据
服务端还有文件和震动 客户端怎么区分发过来的是什么。因为发来的是字节数组
告诉客户端 发的是什么东西
制作一个伪协议
协议是约定 互相都要遵守
在字节数组上做手脚
标记一下 根据类型把字节数据前面的加上一位,[0]为标记 0,文本 1,文件 3,震动
24.对服务端的接收做判断
服务端的接收内做判断
如果是0,按照文本处理,1,按照文件处理,2,按照震动处理
服务端给客户端发信息 btnSend_Click
在buffer中安插一个byte[0]=0;
数组长度不可变,需要新建一个数组来接受,并发送过去
数组长度 buffer.length+1 buffer[0]=0;
赋值给一个新的buffer 使用一个List的泛型集合添加,再转换为字节数组
List<byte> list = new List<byte>();
list.Add(0);
list.AddRange(list);
byte[]newByte= list.ToArray();
最后把传过去的数组改为 dicSocket[ip].Send(newBuffer);
不能直接赋值给buffer,因为长度不一样。
25.返回客户端接收服务器发来的消息 Recive
在接收字符串后得到实际数据大小下面添加判断首数字为(0,1,2)
if(buffer[0]==0)
{
}
把得到文本的处理代码放入这个判断中
然后扩展其他的
else if(buffer==1)
{
}
else if(buffer==2)
{
}
if(r==0)的判断可以放到外面,不用3个都写
if (r == 0)
{
break;
}
处理文本的代码直接套入会有问题,因为字节多了一个首数字,需要算进来
解码的时候要从第二个元素(下标为1)开始处理,因为第一个元素是标记类,没有用
解码多少个,r-1。因为第一个没有用。
string s = Encoding.UTF8.GetString(buffer,1 , r+1);
测试 不会丢失文本
26.服务端选择文件
点击 选择 的时候应该弹出对话框 选择文件 btnSelect事件中 选择发送的文件
OpenFileDialog ofd = new OpenFileDialog();
ofd.Title = "请选择要发送的文件";
ofd.InitialDirectory = @"C:\Users\SJD\Desktop";
ofd.Filter = "所有文件|*.*";
ofd.ShowDialog();
txtPath.Text= ofd.FileName;
27.服务端发送文件
点击 发送文件 btnSend
获得要发送的文件的路径
string path = txtPath.Text;
读取数据流
using (FileStream fsRead = new FileStream(path, FileMode.Open, FileAccess.Read))
{
byte[] buffer = new byte[1024 * 1024 * 5];
int r= fsRead.Read(buffer,0,buffer.Length);
拿到负责通信的socket把字节数组buffer发过去
设置buffer的大小,让他以实际参数大小发过去。
buffer数组,从0到r,无值枚举参数
dicSocket[cboUsers.SelectedItem.ToString()].Send(buffer, 0,r, SocketFlags.None);
}
需要把数值1添加在首位字节,所以
using (FileStream fsRead = new FileStream(path, FileMode.OpenOrCreate, FileAccess.Read))
{
byte[] buffer = new byte[1024 * 1024 * 5];
int r= fsRead.Read(buffer,0,buffer.Length);
//设置buffer的大小,让他以实际参数大小发过去。
List<byte> list = new List<byte>();
list.Add(1);
list.AddRange(buffer);
byte[]newBuffer= list.ToArray();
dicSocket[cboUsers.SelectedItem.ToString()].Send(newBuffer, 0,r+1, SocketFlags.None);
}
28.找到客户端开始接收
在else if(buffer[0]==1)中创建 saveFileDialog的对象
SaveFileDialog sfd = new SaveFileDialog();
sfd.Title = "请选择要保存的文件";
sfd.InitialDirectory = @"C:\Users\SJD\Desktop";
sfd.Filter = "所有文件|*.*";
WIN7,WIN8要加this。否则保存目录弹不出来
sfd.ShowDialog(this);
string path=sfd.FileName;
using (FileStream fsWrite = new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write))
{
fsWrite.Write(buffer, 1, r - 1);
MessageBox.Show("保存成功");
}
会出问题的地方都try-catch一下
29. 发送震动 进入 btnZD_Click事件
创建一位的字节数组,放入2,根据它的键值对发送给指定IP客户端
byte[] buffer = new byte[1];
buffer[0] = 2;
dicSocket[cboUsers.SelectedIte()m.ToString()].Send(buffer);
30.客户端设置震动
客户端创建一个震动的方法
void ZD()
{
循环500次 这个form(客户端)一直不停赋值,位置一直移
for (int i = 0; i < 500; i++)
{
this.Location = new Point(200, 200);
this.Location = new Point(230, 230);
}
进入else if(buffer[0]==2) 把方法 ZD()放进来