C#使用socket实现FTP、POP3、SMTP的客户端 (一)
2017-06-15 21:54
645 查看
概述
socket本质是编程接口,是对TCP/IP的封装。TCP/IP是传输层的协议。
FTP、POP3、SMTP都是应用层的协议,是基于TCP/IP协议的。
所以,我们使用socket实现上述几种协议的客户端,其实是对借助socket对TCP/IP数据传输的封装基础,再往上封装一层的。
(简单说,以FTP为例,就是将FTP中的上传或者下载这类“一次”操作,分解成“多次”的通过socket进行数据的传输罢了。)
FTP客户端
界面图:
控件由以下组成:
五个textbox: tb_IP, tb_port, tb_username, tb_password, tb_path
三个listbox: lsb_local, lsb_server, lsb_status
四个button: btn_conn, btn_setPath, btn_upload, btn_download
该FTP客户端主要实现了建立连接、上传、下载三个button的功能。
头文件:
using System; using System.Windows.Forms; using System.Net.Sockets; using System.IO; using System.Text.RegularExpressions;
Sockets包是肯定要的,IO主要是为了使用NetworkStream类来方便socket的读写,RegularExpressions主要用的是它的split()函数
全局变量:
#region Private variable private TcpClient cmdServer; private TcpClient dataServer; private NetworkStream cmdStrmWtr; private StreamReader cmdStrmRdr; private NetworkStream dataStrmWtr; private StreamReader dataStrmRdr; private String cmdData; private byte[] szData; private const String CRLF = "\r\n"; #endregion
都知道,FTP协议的实现需要建立两个连接,一个21号(通常用21号)端口传输命令,一个随机端口传输数据。所以有两个NetworkStream。
必须注意的是,FTP服务器的命令端口(通常用21号)是保持连接的,数据端口只有在命令端口收到来自Client的请求时才会暂时打开,传输完之后又关闭。
(不了解FTP底层的建议百度“使用telnet执行ftp交互”,代码的实现主要都是通过FTP的命令实现的。)
主要用到的FTP命令如下:
命令 | 描述 |
---|---|
USER <用户名> | 登录FTP的用户名 |
PASS <密码> | 登录FTP的密码 |
QUIT | 断开连接 |
. | . |
PASV | 进入被动模式,返回server的数据端口,等待client连接 |
ABOR | 断开数据端口的连接 |
. | . |
LIST | 查看服务器文件(从数据端口返回结果) |
STOR <文件名> | 请求上传 |
RETR <文件名> | 请求下载 |
全局函数:
#region Private Functions /// <summary> /// 获取命令端口返回结果,并记录在lsb_status上 /// </summary> private String getSatus() { String ret = cmdStrmRdr.ReadLine(); lsb_status.Items.Add(ret); lsb_status.SelectedIndex = lsb_status.Items.Count - 1; return ret; } /// <summary> /// 进入被动模式,并初始化数据端口的输入输出流 /// </summary> private void openDataPort() { string retstr; string[] retArray; int dataPort; // Start Passive Mode cmdData = "PASV" + CRLF; szData = System.Text.Encoding.ASCII.GetBytes(cmdData.ToCharArray()); cmdStrmWtr.Write(szData, 0, szData.Length); retstr = this.getSatus(); // Calculate data's port retArray = Regex.Split(retstr, ","); if (retArray[5][2] != ')') retstr = retArray[5].Substring(0, 3); else retstr = retArray[5].Substring(0, 2); dataPort = Convert.ToInt32(retArray[4]) * 256 + Convert.ToInt32(retstr); lsb_status.Items.Add("Get dataPort=" + dataPort); //Connect to the dataPort dataServer = new TcpClient(tb_IP.Text, dataPort); dataStrmRdr = new StreamReader(dataServer.GetStream()); dataStrmWtr = dataServer.GetStream(); } /// <summary> /// 断开数据端口的连接 /// </summary> private void closeDataPort() { dataStrmRdr.Close(); dataStrmWtr.Close(); this.getSatus(); cmdData = "ABOR" + CRLF; szData = System.Text.Encoding.ASCII.GetBytes(cmdData.ToCharArray()); cmdStrmWtr.Write(szData, 0, szData.Length); this.getSatus(); } /// <summary> /// 获得/刷新 右侧的服务器文件列表 /// </summary> private void freshFileBox_Right() { openDataPort(); string absFilePath; //List cmdData = "LIST" + CRLF; szData = System.Text.Encoding.ASCII.GetBytes(cmdData.ToCharArray()); cmdStrmWtr.Write(szData, 0, szData.Length); this.getSatus(); lsb_server.Items.Clear(); while ((absFilePath = dataStrmRdr.ReadLine()) != null) { string[] temp = Regex.Split(absFilePath, " "); lsb_server.Items.Add(temp[temp.Length - 1]); } closeDataPort(); } /// <summary> /// 获得/刷新 左侧的本地文件列表 /// </summary> private void freshFileBox_Left() { lsb_local.Items.Clear(); if (tb_path.Text == "") return; var files = Directory.GetFiles(tb_path.Text, "*.*"); foreach (var file in files) { Console.WriteLine(file); string[] temp = Regex.Split(file, @"\\"); lsb_local.Items.Add(temp[temp.Length - 1]); } } #endregion
重用部分的代码太多了,就把它们拉出来写成了全局函数,所以类型大多都是void,通过全局变量传递结果,这样做还是省了很多行代码的(虽然事实上整个代码看起来还是挺冗杂的)
连接按键(btn_conn):
#region Button: Connect & Disconnect private void btn_conn_Click(object sender, EventArgs e) { if (btn_conn.Text == "连接") { Cursor cr = Cursor.Current; Cursor.Current = Cursors.WaitCursor; cmdServer = new TcpClient(tb_IP.Text, Convert.ToInt32(tb_port.Text)); lsb_status.Items.Clear(); try { cmdStrmRdr = new StreamReader(cmdServer.GetStream()); cmdStrmWtr = cmdServer.GetStream(); this.getSatus(); string retstr; //Login cmdData = "USER " + tb_username.Text + CRLF; szData = System.Text.Encoding.ASCII.GetBytes(cmdData.ToCharArray()); cmdStrmWtr.Write(szData, 0, szData.Length); this.getSatus(); cmdData = "PASS " + tb_password.Text + CRLF; szData = System.Text.Encoding.ASCII.GetBytes(cmdData.ToCharArray()); cmdStrmWtr.Write(szData, 0, szData.Length); retstr = this.getSatus().Substring(0, 3); if (Convert.ToInt32(retstr) == 530) throw new InvalidOperationException("帐号密码错误"); this.freshFileBox_Right(); lb_IP.Text = tb_IP.Text + ":"; btn_conn.Text = "断开"; btn_upload.Enabled = true; btn_download.Enabled = true; } catch (InvalidOperationException err) { lsb_status.Items.Add("ERROR: " + err.Message.ToString()); } finally { Cursor.Current = cr; } } else { Cursor cr = Cursor.Current; Cursor.Current = Cursors.WaitCursor; //Logout cmdData = "QUIT" + CRLF; szData = System.Text.Encoding.ASCII.GetBytes(cmdData.ToCharArray()); cmdStrmWtr.Write(szData, 0, szData.Length); this.getSatus(); cmdStrmWtr.Close(); cmdStrmRdr.Close(); lb_IP.Text = ""; btn_conn.Text = "连接"; btn_upload.Enabled = false; btn_download.Enabled = false; lsb_server.Items.Clear(); Cursor.Current = cr; } } #endregion
代码丑归丑……逻辑还是挺明确的。就是建立连接发送“用户名+密码”,断开连接就发送“QUIT”。
设置路径按键(btn_setPath)
#region Button: Set Path private void btn_setPath_Click(object sender, EventArgs e) { string path = string.Empty; FolderBrowserDialog fbd = new FolderBrowserDialog(); if (fbd.ShowDialog() == DialogResult.OK) { path = fbd.SelectedPath; lsb_status.Items.Add("选中本地路径:" + path); } tb_path.Text = path; freshFileBox_Left(); } #endregion
第二个键,也没啥好说的,代码很短,看了就懂了
上传按键(btn_upload)&下载按键(btn_download):
#region Button: upload & download /// <summary> /// 上传 /// </summary> private void btn_upload_Click(object sender, EventArgs e) { if (tb_path.Text == "" || lsb_local.SelectedIndex < 0) { MessageBox.Show("请选择上传的文件", "ERROR"); return; } Cursor cr = Cursor.Current; Cursor.Current = Cursors.WaitCursor; string fileName = lsb_local.Items[lsb_local.SelectedIndex].ToString(); string filePath = tb_path.Text + "\\" + fileName; this.openDataPort(); cmdData = "STOR " + fileName + CRLF; szData = System.Text.Encoding.ASCII.GetBytes(cmdData.ToCharArray()); cmdStrmWtr.Write(szData, 0, szData.Length); this.getSatus(); FileStream fstrm = new FileStream(filePath, FileMode.Open); byte[] fbytes = new byte[1030]; int cnt = 0; while ((cnt = fstrm.Read(fbytes, 0, 1024)) > 0) { dataStrmWtr.Write(fbytes, 0, cnt); } fstrm.Close(); this.closeDataPort(); this.freshFileBox_Right(); Cursor.Current = cr; } /// <summary> /// 下载 /// </summary> private void btn_download_Click(object sender, EventArgs e) { if (tb_path.Text == "" || lsb_server.SelectedIndex < 0) { MessageBox.Show("请选择目标文件和下载路径", "ERROR"); return; } Cursor cr = Cursor.Current; Cursor.Current = Cursors.WaitCursor; string fileName = lsb_server.Items[lsb_server.SelectedIndex].ToString(); string filePath = tb_path.Text + "\\" + fileName; this.openDataPort(); cmdData = "RETR " + fileName + CRLF; szData = System.Text.Encoding.ASCII.GetBytes(cmdData.ToCharArray()); cmdStrmWtr.Write(szData, 0, szData.Length); this.getSatus(); FileStream fstrm = new FileStream(filePath, FileMode.OpenOrCreate); char[] fchars = new char[1030]; byte[] fbytes = new byte[1030]; int cnt = 0; while ((cnt = dataStrmWtr.Read(fbytes, 0, 1024)) > 0) { fstrm.Write(fbytes, 0, cnt); } fstrm.Close(); this.closeDataPort(); this.freshFileBox_Left(); Cursor.Current = cr; } #endregion
“下载”操作相当于是服务器端读取目标文件,然后把读到的内容通过数据端口发送给客户端,客户端读到数据后就写到本地。(跟传真一样)
同理,“上传”操作将这个过程反过来了。
注意到“download”里面竟然是用dataStrmWtr(NetworkStream类)来读取来自服务器的数据,而不是dataStrmRdr(StreamReader类)。
因为后者的Read()函数和ReadLine()函数读取的是经过转换的char[]类型数组或者string类,而前者读取的是未经过转换的byte[]类型数组。
如果是为了解析服务器传过来的内容,当然是直接读使用StreamReader类来读到socket传来的string类。但如果是传文件的话,必须用byte[],读多少byte,就通过FileStream写多少byte到本地。否则得到的文件相当于进行了两次转码(从byte到char,再从char转回byte),文件必然会失真。
POP3客户端
C#使用socket实现FTP、POP3、SMTP的客户端 (二)SMTP客户端
C#使用socket实现FTP、POP3、SMTP的客户端 (三)相关文章推荐
- C#使用socket实现FTP、POP3、SMTP的客户端 (二)
- C#使用socket实现FTP、POP3、SMTP的客户端 (三)
- 使用 Socket 通信实现 FTP 客户端程序
- 使用 Socket 通信实现 FTP 客户端程序
- 使用 Socket 通信实现 FTP 客户端程序
- 使用 Socket 通信实现 FTP 客户端程序
- 使用 Socket 通信实现 FTP 客户端程序(来自IBM)
- 使用 Socket 通信实现 FTP 客户端程序
- 使用 Socket 通信实现 FTP 客户端程序
- 使用 Socket 通信实现 FTP 客户端程序
- 使用 Socket 通信实现 FTP 客户端程序
- 使用 Socket 通信实现 FTP 客户端程序
- 使用 Socket 通信实现 FTP 客户端程序
- 使用 Socket 通信实现 FTP 客户端程序
- 使用 Socket 通信实现 FTP 客户端程序
- 使用 Socket 通信实现 FTP 客户端程序
- 使用 Socket 通信实现 FTP 客户端程序
- 使用C语言socket实现windows pc与ftp服务器通信---socket实现ftp客户端
- 使用 Socket 通信实现 FTP 客户端程序
- 使用 Socket 通信实现 FTP 客户端程序