1.進程複習//通過進程去打開應用程式 Process.Start("calc"); Process.Start("mspaint"); Process.Start("notepad"); Process.Start("iexplore", "http://www.baidu.com"); //通過 ...
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()放進來