C#中Socket通信编程的同步实现
目录(?)[+]
- Socket编程基础
- 什么是Socket编程的同步实现
- 如何实现Socket同步通信
- 服务端
- 基本流程
- 代码示例
- 客户端
- 基本流程
- 代码示例
- 服务端
- 总结
本文通过分析和总结C#中Socket通信编程的关键技术,按照同步实现的方式实现了一个简单的Socket聊天程序,目的是通过这个程序来掌握Socket编程,为进一步开发Unity3D网络游戏打下一个坚实的基础。
Socket编程基础
关于Socket编程基础部分的内容,主要是了解和掌握.NET框架下为Socket编程提供的相关类和接口方法。.NET中常见的网络相关的API都集中在System.NET和System.Net.Socket这两个命名空间下,大家可以通过MSDN去了解这两个命名空间下相关的类和方法。这里援引一位朋友总结的一篇文章http://www.cnblogs.com/sunev/archive/2012/08/05/2604189.html,大家可以从这里获得更为直观的认识。
什么是Socket编程的同步实现
本文的目的是按照同步实现的方式来实现一个简单的Socket聊天程序,因此在解决这个问题前,我们首先来看看什么是Socket编程的同步实现。所谓Socket编程的同步实现就是指按照同步过程的方法来实现Socket通信。从编程来说,我们常用的方法或者函数都是同步过程。因为当我们调用一个方法或者函数的时候我们能够立即得到它的返回值。可是我们知道在Socket通信中,我们不能保证时时刻刻连接都通畅、更不能够保证时时刻刻都有数据收发,因为我们就需要不断去读取相应的值来确定整个过程的状态。这就是Socket编程的同步实现了,下面我们来看具体的实现过程。
如何实现Socket同步通信
服务端
服务端的主要职责是处理各个客户端发送来的数据,因此在客户端的Socket编程中需要使用两个线程来循环处理客户端的请求,一个线程用于监听客户端的连接情况,一个线程用于监听客户端的消息发送,当服务端接收到客户端的消息后需要将消息处理后再分发给各个客户端。
基本流程
- 创建套接字
- 绑定套接字的IP和端口号——Bind()
- 将套接字处于监听状态等待客户端的连接请求——Listen()
- 当请求到来后,接受请求并返回本次会话的套接字——Accept()
- 使用返回的套接字和客户端通信——Send()/Receive()
- 返回,再次等待新的连接请求
- 关闭套接字
代码示例
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;namespace TCPLib
{public class TCPServer{private byte[] result = new byte[1024];/// <summary>/// 最大的监听数量/// </summary>private int maxClientCount;public int MaxClientCount{get { return maxClientCount; }set { maxClientCount = value; }}/// <summary>/// IP地址/// </summary>private string ip;public string IP{get { return ip; }set { ip = value; }}/// <summary>/// 端口号/// </summary>private int port;public int Port{get { return port; }set { port = value; }}/// <summary>/// 客户端列表/// </summary>private List<Socket> mClientSockets;public List<Socket> ClientSockets{get { return mClientSockets; }}/// <summary>/// IP终端/// </summary>private IPEndPoint ipEndPoint;/// <summary>/// 服务端Socket/// </summary>private Socket mServerSocket;/// <summary>/// 当前客户端Socket/// </summary>private Socket mClientSocket;public Socket ClientSocket {get { return mClientSocket; }set { mClientSocket = value; }}/// <summary>/// 构造函数/// </summary>/// <param name="port">端口号</param>/// <param name="count">监听的最大树目</param>public TCPServer(int port, int count){this.ip = IPAddress.Any.ToString();this.port = port;this.maxClientCount=count;this.mClientSockets = new List<Socket>();//初始化IP终端this.ipEndPoint = new IPEndPoint(IPAddress.Any, port);//初始化服务端Socketthis.mServerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//端口绑定this.mServerSocket.Bind(this.ipEndPoint);//设置监听数目this.mServerSocket.Listen(maxClientCount);}/// <summary>/// 构造函数/// </summary>/// <param name="ip">ip地址</param>/// <param name="port">端口号</param>/// <param name="count">监听的最大数目</param>public TCPServer(string ip,int port,int count){this.ip = ip;this.port = port;this.maxClientCount = count;this.mClientSockets = new List<Socket>();//初始化IP终端this.ipEndPoint = new IPEndPoint(IPAddress.Parse(ip), port);//初始化服务端Socketthis.mServerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//端口绑定this.mServerSocket.Bind(this.ipEndPoint);//设置监听数目this.mServerSocket.Listen(maxClientCount);}/// <summary>/// 定义一个Start方法将构造函数中的方法分离出来/// </summary>public void Start(){//创建服务端线程,实现客户端连接请求的循环监听var mServerThread = new Thread(this.ListenClientConnect);//服务端线程开启mServerThread.Start();}/// <summary>/// 监听客户端链接/// </summary>private void ListenClientConnect(){//设置循环标志位bool flag = true;while (flag){//获取连接到服务端的客户端this.ClientSocket = this.mServerSocket.Accept();//将获取到的客户端添加到客户端列表this.mClientSockets.Add(this.ClientSocket);//向客户端发送一条消息this.SendMessage(string.Format("客户端{0}已成功连接到服务器", this.ClientSocket.RemoteEndPoint));//创建客户端消息线程,实现客户端消息的循环监听var mReveiveThread = new Thread(this.ReceiveClient);//注意到ReceiveClient方法传入了一个参数//实际上这个参数就是此时连接到服务器的客户端//即ClientSocketmReveiveThread.Start(this.ClientSocket);}}/// <summary>/// 接收客户端消息的方法/// </summary>private void ReceiveClient(object obj){//获取当前客户端//因为每次发送消息的可能并不是同一个客户端,所以需要使用var来实例化一个新的对象//可是我感觉这里用局部变量更好一点var mClientSocket = (Socket)obj;// 循环标志位bool flag = true;while (flag){try{//获取数据长度int receiveLength = mClientSocket.Receive(result);//获取客户端消息string clientMessage = Encoding.UTF8.GetString(result, 0, receiveLength);//服务端负责将客户端的消息分发给各个客户端this.SendMessage(string.Format("客户端{0}发来消息:{1}",mClientSocket.RemoteEndPoint,clientMessage));}catch (Exception e){//从客户端列表中移除该客户端this.mClientSockets.Remove(mClientSocket);//向其它客户端告知该客户端下线this.SendMessage(string.Format("服务器发来消息:客户端{0}从服务器断开,断开原因:{1}",mClientSocket.RemoteEndPoint,e.Message));//断开连接mClientSocket.Shutdown(SocketShutdown.Both);mClientSocket.Close();break;}}}/// <summary>/// 向所有的客户端群发消息/// </summary>/// <param name="msg">message</param>public void SendMessage(string msg){//确保消息非空以及客户端列表非空if (msg == string.Empty || this.mClientSockets.Count <= 0) return;//向每一个客户端发送消息foreach (Socket s in this.mClientSockets){(s as Socket).Send(Encoding.UTF8.GetBytes(msg));}}/// <summary>/// 向指定的客户端发送消息/// </summary>/// <param name="ip">ip</param>/// <param name="port">port</param>/// <param name="msg">message</param>public void SendMessage(string ip,int port,string msg){//构造出一个终端地址IPEndPoint _IPEndPoint = new IPEndPoint(IPAddress.Parse(ip), port);//遍历所有客户端foreach (Socket s in mClientSockets){if (_IPEndPoint == (IPEndPoint)s.RemoteEndPoint){s.Send(Encoding.UTF8.GetBytes(msg));}}}}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
- 209
- 210
- 211
- 212
- 213
- 214
- 215
- 216
- 217
- 218
- 219
- 220
- 221
- 222
- 223
- 224
- 225
- 226
- 227
- 228
好了,现在我们已经编写好了一个具备接收和发送数据能力的服务端程序。现在我们来尝试让服务端运行起来:
using System;
using System.Collections.Generic;
using System.Text;
using TCPLib;
using System.Net;
using System.Net.Sockets;namespace TCPLib.Test
{class Program{static void Main(string[] args){//指定IP和端口号及最大监听数目的方式TCPLib.TCPServer s1 = new TCPServer("127.0.0.1", 6001, 10);//指定端口号及最大监听数目的方式TCPLib.TCPServer s2 = new TCPServer(6001, 10);//执行Start方法s1.Start();}}}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
现在我们来看看编写客户端Socket程序的基本流程
客户端
客户端相对于服务端来说任务要轻许多,因为客户端仅仅需要和服务端通信即可,可是因为在和服务器通信的过程中,需要时刻保持连接通畅,因此同样需要两个线程来分别处理连接情况的监听和消息发送的监听。
基本流程
- 创建套接字保证与服务器的端口一致
- 向服务器发出连接请求——Connect()
- 和服务器端进行通信——Send()/Receive()
- 关闭套接字
代码示例
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;namespace TCPLib
{public class TCPClient{/// <summary>/// 定义数据/// </summary>private byte[] result = new byte[1024];/// <summary>/// 客户端IP/// </summary>private string ip;public string IP{get { return ip; }set { ip = value; }}/// <summary>/// 客户端端口号/// </summary>private int port;public int Port{get { return port; }set { port = value; }}/// <summary>/// IP终端/// </summary>private IPEndPoint ipEndPoint;/// <summary>/// 客户端Socket/// </summary>private Socket mClientSocket;/// <summary>/// 是否连接到了服务器/// 默认为flase/// </summary>private bool isConnected = false;/// <summary>/// 构造函数/// </summary>/// <param name="ip">IP地址</param>/// <param name="port">端口号</param>public TCPClient(string ip, int port){this.ip=ip;this.port=port;//初始化IP终端this.ipEndPoint = new IPEndPoint(IPAddress.Parse(this.ip), this.port);//初始化客户端SocketmClientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);}public void Start(){//创建一个线程以不断连接服务器var mConnectThread = new Thread(this.ConnectToServer);//开启线程mConnectThread.Start();}/// <summary>/// 连接到服务器/// </summary>private void ConnectToServer(){//当没有连接到服务器时开始连接while (!isConnected){try{//开始连接mClientSocket.Connect(this.ipEndPoint);this.isConnected = true;}catch (Exception e){//输出Debug信息Console.WriteLine(string.Format("因为一个错误的发生,暂时无法连接到服务器,错误信息为:{0}",e.Message));this.isConnected = false;}//等待5秒钟后尝试再次连接Thread.Sleep(5000);Console.WriteLine("正在尝试重新连接...");}//连接成功后Console.WriteLine("连接服务器成功,现在可以和服务器进行会话了");//创建一个线程以监听数据接收var mReceiveThread = new Thread(this.ReceiveMessage);//开启线程mReceiveThread.Start();}/// <summary>/// 因为客户端只接受来自服务器的数据/// 因此这个方法中不需要参数/// </summary>private void ReceiveMessage(){//设置循环标志位bool flag = true;while (flag){try{//获取数据长度int receiveLength = this.mClientSocket.Receive(result);//获取服务器消息string serverMessage = Encoding.UTF8.GetString(result, 0, receiveLength);//输出服务器消息Console.WriteLine(serverMessage);}catch (Exception e){//停止消息接收flag = false;//断开服务器this.mClientSocket.Shutdown(SocketShutdown.Both);//关闭套接字this.mClientSocket.Close();//重新尝试连接服务器this.isConnected = false;ConnectToServer();}}}/// <summary>/// 发送消息/// </summary>/// <param name="msg">消息文本</param>public void SendMessage(string msg){if(msg==string.Empty || this.mClientSocket==null) return;mClientSocket.Send(Encoding.UTF8.GetBytes(msg));}}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
同样地,我们现在来运行客户端程序,这样客户端就可以和服务端进行通信了:
using System;
using System.Collections.Generic;
using System.Text;
using TCPLib;
using System.Net;
using System.Net.Sockets;namespace TCPLib.Test
{class Program{static void Main(string[] args){//保证端口号和服务端一致TCPLib.TCPClient c = new TCPClient("127.0.0.1",6001);//执行Start方法c.Start();while(true){//读取客户端输入的消息string msg = Console.ReadLine();//发送消息到服务端c.SendMessage(msg);}}}}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
注意要先运行服务端的程序、再运行客户端的程序,不然程序会报错,嘿嘿!好了,下面是今天的效果演示图:
总结
今天我们基本上写出了一个可以使用的用例,不过这个例子目前还存在以下问题:
* 这里仅仅实现了发送字符串的功能,如何让这个程序支持更多的类型,从基础的int、float、double、string、single等类型到structure、class甚至是二进制文件的类型?
* 如何让这个用例更具有扩展性,我们发现所有的Socket编程流程都是一样的,唯一不同就是在接收到数据以后该如何去处理,因为能不能将核心功能和自定义功能分离开来?
* 在今天的这个用例中,数据传输的缓冲区大小我们人为设定为1024,那么如果碰到比这个设定更大的数据类型,这个用例该怎么来写?
好了,这就是今天的内容了,希望大家喜欢,同时希望大家关注我的博客!
C#中Socket通信编程的同步实现相关推荐
- C#中Socket通信编程的异步实现
http://blog.csdn.net/mss359681091/article/details/51790931 转载于:https://www.cnblogs.com/rwzhou/p/6834 ...
- Java中Socket通信-客户端与服务端相互传输对象数据
场景 Java中Socket通信-服务端和客户端双向传输字符串实现: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/1084885 ...
- Java中Socket通信-客户端向服务端发送照片
场景 Java中Socket通信-服务端和客户端双向传输字符串实现: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/1084885 ...
- C#中串口通信编程 收藏
C#中串口通信编程 收藏 本文将介绍如何在.NET平台下使用C#创建串口通信程序,.NET 2.0提供了串口通信的功能,其命名 空间是System.IO.Ports.这个新的框架不但可以访问计算机上的 ...
- C#中Socket通信用法实例详解
本文实例讲述了C#中Socket通信用法.分享给大家供大家参考.具体如下: 一.UDP方式: 服务器端代码: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 1 ...
- java面试题44关于 Socket 通信编程,以下描述正确的是:( )
java面试题44关于 Socket 通信编程,以下描述正确的是:( ) A 客户端通过new ServerSocket()创建TCP连接对象 B 客户端通过TCP连接对象调用accept()方法创建 ...
- Android中socket通信简单实现
Android中socket通信简单实现 socket通信需要有一个服务器和客户端,可以把同一个APP作为服务器跟客户端,也可以分开成两个APP. 先上个图: 这里以一个APP作为服务器跟客户端为示例 ...
- C#中Socket多线程编程实例
C#是微软随着VS.net新推出的一门语言.它作为一门新兴的语言,有着C++的强健,又有着VB等的RAD特性.而且,微软推出C#主要的目的是为了对抗Sun公司的Java.大家都知道Java语言的强大功 ...
- Java中Socket通信-服务端和客户端双向传输字符串实现
场景 什么是socket 在计算机通信领域,socket 被翻译为"套接字",它是计算机之间进行通信的一种约定或一种方式.通过 socket 这种约定,一台计算机可以接收其他计算机 ...
最新文章
- securecrt连接mysql_使用SecureCRTP 连接生产环境的web服务器和数据库服务器
- 老李案例分享:Weblogic性能优化案例
- 【Flask】Flask-RESTful 风格编程
- My Goal For SE
- elasticsearch5安装和elasticsearch-analysis-ik中文分词插件安装
- 二十种实战调优MySQL性能优化的经验(转自公众号:架构之家 2018-05-08)
- [渝粤教育] 西南科技大学 高级英语(2) 在线考试复习资料
- HTTP 和 HTTPS 协议
- mysql开启权限控制_mysql开启远程访问及相关权限控制
- androidStudio导入库文件
- 作业4:结对编程项目四则运算
- 电商Sass平台-商城运营后台原型-仓储管理-订单管理-店铺运营-采购管理-数据分析-交易分析-留存分析-客户管理-用户运营-围栏管理-商品管理-流量分析-电商erp后台管理-用户权限-销量分析
- css3-13 css3的3D动画如何实现
- Spring 加载Bean流程
- sublime3dsmax - Sublime Text Send To 3ds Max 解决中文路径问题
- 一个美国人到中国当产品经理的心得:中国App设计真好!
- xshell的快捷复制粘贴设置(*)
- 数据通信与网络有这一篇就够了(网络模型篇)
- CA搭建实验和安全基础
- 51CTO学院周年庆开始了!