梦想成真—Socket技术详解
1.序
1.1计算机网络
计算机网络是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统,网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。
计算机网络的分类与一般的事物分类方法一样,可以按事物所具有的不同性质特点分类。计算机网络通俗地讲就是由多台计算机通过传输介质和软件物理连接在一起组成的。总的来说计算机网络的组成基本上包括:计算机、网络操作系统、传输介质以及相应的应用软件四部分。
1.2 网络编程
网络编程最主要的工作就是在发送端把信息通过规定好的协议进行组装包,在接收端按照规定好的协议把包进行解析,从而提取出对应的信息,达到通信的目的。中间最主要的就是数据包的组装,数据包的过滤,数据包的捕获,数据包的分析,当然最后再做一些处理,代码、开发工具、数据库、服务器架设和网页设计这5部分你都要接触。
2.Socket技术
对TCP/IP、UDP、Socket编程这些词你不会很陌生吧?随着网络技术的发展,这些词充斥着我们的耳朵。那么我想问:
1.什么是TCP/IP、UDP?
2.Socket在哪里呢?
3.Socket是什么呢?
4.你会使用它们吗?
先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
2.1原理
什么是Socket?
在socket网络编程中,都是端到端通信,由客户端端口+服务端端口+客户端IP+服务端IP+传输协议组成的五元组可以明确的标识一条连接。
上面我们已经知道网络中的进程是通过socket来通信的,那什么是socket呢?socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),这些函数我们在后面进行介绍
2.2socket的基本操作
既然socket是“open—write/read—close”模式的一种实现,那么socket就提供了这些操作对应的函数接口。下面以TCP为例,介绍几个基本的socket接口函数。
2.2.1 socket()函数
int socket(int domain, int type, int protocol);
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
它的参数
domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)。
sock_stream是有保障的(即能保证数据正确传送到对方)面向连接的SOCKET,多用于资料(如文件)传送。
sock_dgram是无保障的面向消息的socket , 主要用于在网络上发广播信息。
对比SOCK_STREAM是基于TCP的,数据传输比较有保障。SOCK_DGRAM是基于UDP的,专门用于局域网,基于广播 SOCK_STREAM 是数据流,一般是tcp/ip协议的编程,
(补充知识点)
字节流是无边界的,SOCK_DGRAM分是数据报,是udp协议网络编程,数据报是记录型的数据报,所谓的记录型数据报就是接收进程可以识别接收到的数据报的记录边界。
protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议
注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0 时,会自动选择type类型对应的默认协议。
2.2.2 bind()函数
正如上面所说bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数的三个参数分别为:
sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是:
如ipv4对应的是:
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
};
struct in_addr {
uint32_t s_addr;
};
ipv6对应的是:
struct sockaddr_in6 {
sa_family_t sin6_family;
in_port_t sin6_port;
uint32_t sin6_flowinfo;
struct in6_addr sin6_addr;
uint32_t sin6_scope_id;
};
struct in6_addr {
unsigned char s6_addr[16];
};
Unix域对应的是:
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family;
char sun_path[UNIX_PATH_MAX];
};
addrlen:对应的是地址的长度。
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
2.2.3 listen()、connect()函数
如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。
2.2.4 accept()函数
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
2.2.5 read()、write()等函数
万事具备只欠东风,至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组:
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
C#中recv()/send()
我推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。它们的声明如下:
sendmsg和recvmsg这两个接口是高级套接口,这两个接口支持一般数据的发送和接收,还支持多缓冲区的报文发送和接收(readv和sendv支持多缓冲区发送和接收),还可以在报文中带辅助数据。这些功能是常用的send、recv等接口无法完成的。
.
- ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
- ssize_t sendmsg(int sockfd, struct msghdr *msg, int flags);
2.3实战
此通信是WPF与Unity3D之间的一个通信
此项目是基于WPF界面的实现。其中嵌入了Unity3D仿真。但是Unity3D与WPF的通信是不能直接进行的,有两种方法,一种是通过数据库,WPF给数据库中插入指令,Unity3D不断的遍历数据库,一旦发现有指令就进行某些操作,第二种方法是Socket通信。接下来进行对Socket通信的讲解。在这里插入代码片
2.3.1发生端(WPF)
```csharp
class SocketServer
{
//私有成员
private static byte[] result = new byte[1024];
private int myProt = 500; //端口
static Socket serverSocket;
static Socket clientSocket;
Thread myThread;
static Thread receiveThread;
//属性
public int port {
get; set; }
//方法
static SocketServer socketServer = null;
public static SocketServer GetInstance()
{
if(socketServer == null)
{
socketServer = new SocketServer();
socketServer.StartServer();
}
return socketServer;
}
SocketServer()
{
}
void StartServer()
{
//服务器IP地址
IPAddress ip = IPAddress.Parse("127.0.0.1");
serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
serverSocket.Bind(new IPEndPoint(ip, myProt)); //绑定IP地址:端口
serverSocket.Listen(10); //设定最多10个排队连接请求
Debug.WriteLine("启动监听{0}成功", serverSocket.LocalEndPoint.ToString());
//通过Clientsoket发送数据
myThread = new Thread(ListenClientConnect);
myThread.Start();
}
internal void QuitServer()
{
if (serverSocket != null)
serverSocket.Close();
if (clientSocket != null)
clientSocket.Close();
if(myThread != null)
myThread.Abort();
if(receiveThread != null)
receiveThread.Abort();
}
internal void SendMessage(string msg)
{
if(clientSocket != null && clientSocket.Connected)
clientSocket.Send(Encoding.ASCII.GetBytes(msg));
}
/// <summary>
/// 监听客户端连接
/// </summary>
private static void ListenClientConnect()
{
while (true)
{
try
{
clientSocket = serverSocket.Accept();
clientSocket.Send(Encoding.ASCII.GetBytes("Server Say Hello"));
receiveThread = new Thread(ReceiveMessage);
receiveThread.Start(clientSocket);
}
catch (Exception)
{
}
}
}
/// <summary>
/// 接收消息
/// </summary>
/// <param name="clientSocket"></param>
private static void ReceiveMessage(object clientSocket)
{
Socket myClientSocket = (Socket)clientSocket;
while (true)
{
try
{
//通过clientSocket接收数据
int receiveNumber = myClientSocket.Receive(result);
Debug.WriteLine("接收客户端{0}消息{1}", myClientSocket.RemoteEndPoint.ToString(), Encoding.ASCII.GetString(result, 0, receiveNumber));
}
catch (Exception ex)
{
try
{
Debug.WriteLine(ex.Message);
myClientSocket.Shutdown(SocketShutdown.Both);
myClientSocket.Close();
break;
}
catch (Exception)
{
}
}
}
}
public void CloseClient()
{
if (clientSocket != null && clientSocket.Connected)
{
clientSocket.Close();
clientSocket = null;
}
}
}
2.3.2发送程序
```csharp
public partial class MainWindow : Window
{
SocketServer socketServer = null;
public MainWindow()
{
InitializeComponent();
socketServer = SocketServer.GetInstance();
}
public void SendMessage(string message)
{
socketServer.SendMessage(message);
}
public void CloseWindow()
{
socketServer.QuitServer();
CloseScene();
Close();
}
public void CloseScene()
{
socketServer.CloseClient();
UnityControl.closeUnity3dExe();
CloseTabControl("三维仿真场景");
}
}
2.3.3接收端(Unity3D)
using UnityEngine;
using System.Collections;
using System.Net.Sockets;
using System;
using System.Collections.Generic;
public class SocketCommand : MonoBehaviour
{
const int portNo = 500;
private TcpClient _client;
byte[] data;
string Error_Message;
List<string> messages = new List<string>();
float checkTime = 0.1f;
void Start()
{
try
{
this._client = new TcpClient();
this._client.Connect("127.0.0.1", portNo);
data = new byte[this._client.ReceiveBufferSize];
//SendMessage(txtNick.Text);
SendMessage("Unity Demo Client is Ready!");
this._client.GetStream().BeginRead(data, 0, System.Convert.ToInt32(this._client.ReceiveBufferSize), ReceiveMessage, null);
}
catch (Exception ex)
{
}
}
void FixedUpdate()
{
checkTime -= Time.fixedDeltaTime;
if(checkTime <= 0)
{
checkTime = 0.1f;
MessageDealing();
}
}
/// <summary>
/// 对收到的消息的处理函数
/// </summary>
private void MessageDealing()
{
if(messages.Count > 0)
{
Debug.Log(messages[0]);
Command.CommandDecoding(messages[0]);
messages.Remove(messages[0]);
}
}
public new void SendMessage(string message)
{
try
{
NetworkStream ns = this._client.GetStream();
byte[] data = System.Text.Encoding.ASCII.GetBytes(message);
ns.Write(data, 0, data.Length);
ns.Flush();
}
catch (Exception ex)
{
Error_Message = ex.Message;
//MessageBox.Show(ex.ToString());
}
}
public void ReceiveMessage(IAsyncResult ar)
{
try
{
//清空errormessage
Error_Message = "";
int bytesRead;
bytesRead = this._client.GetStream().EndRead(ar);
if (bytesRead < 1)
{
return;
}
else
{
// Debug.Log(System.Text.Encoding.ASCII.GetString(data, 0, bytesRead));
string message = System.Text.Encoding.ASCII.GetString(data, 0, bytesRead);
messages.Add(message);
}
this._client.GetStream().BeginRead(data, 0, System.Convert.ToInt32(this._client.ReceiveBufferSize), ReceiveMessage, null);
}
catch (Exception ex)
{
Error_Message = ex.Message;
}
}
public void CloseClient()
{
if(_client.Connected)
this._client.Close();
}
void OnDestroy()
{
if (_client.Connected)
this._client.Close();
}
}
3.总结
本人面试得益于socket 技术。因为我的项目中使用了该技术,所以在面试中就讲解对于这一块的理解,并且在面试之前也大概深入的看了一下。所以这个也告诉我们对于自己准备的项目一定要对于其中的技术有所思考,有所深入,让面试官看到你的潜力以及可塑性。既然你对研究生期间所做的项目进行了深入的研究,那么在进入工作之后依然会做的很好。所以加油,希望各位梦想成真!!!
4.关注我
博客地址
https://blog.csdn.net/weixin_41563161
掘金https://juejin.cn/user/2814360172369271
知乎https://www.zhihu.com/people/hai-kuo-tian-kong-63-38-21
公众号