本文详细阐述了Socket通信的原理和实现,并通过实例代码进行了详细介绍。对大家的学习或者工作都有一定的参考价值,有需要的朋友可以参考一下。
目录
TCP/IP,UDP,Socket什么是TCP/IP,UDP?插座在哪里?什么是插座?你能使用它们吗?1.进程如何在网络中通信?2.什么是插座?单词socket的由来3、socket的基本操作3.1、socket()函数3.2、bind()函数网络字节顺序和主机字节顺序3.3、listen()、connect()函数3.4、accept()函数3.5、read()、write()等函数3.6、close()函数4、TCP in socket
TCP/IP、UDP、Socket
TCP/IP,UDP,Socket编程这些词你熟悉吗?随着网络技术的发展,这些词汇充斥着我们的耳朵。然后我想问:
1.什么是TCP/IP,UDP?2.插座在哪里?3.什么是插座?4.你能使用它们吗?
什么是TCP/IP、UDP?
TCP/IP(传输控制协议/互联网协议),即传输控制协议/互联网协议,是一个工业标准协议集,是为广域网(wan)设计的。
UDP(用户数据协议)是与TCP相对应的协议。它属于TCP/IP协议家族。
下图显示了这些协议之间的关系。
图1
TCP/IP协议族包括传输层、网络层和链路层。现在你知道TCP/IP和UDP的关系了。
Socket在哪里呢?
在图1中,我们没有看到Socket的影子,那么它在哪里呢?还是用图片说话,一目了然。
图2
所以插座在这里。
Socket是什么呢?
它是Socket的应用层和TCP/IP协议族之间的中间软件抽象层,是一组接口。在设计模式中,Socket实际上是一个facade模式,它将复杂的TCP/IP协议族隐藏在Socket接口后面。对于用户来说,一套简单的接口就足够了,这样套接字就可以组织数据以符合指定的协议。
你会使用它们吗?
前辈为我们做了很多事情,网络之间的沟通也简单了很多,但是还有很多工作要做。之前听到Socket编程的时候,我以为是比较高深的编程知识,但是只要知道Socket编程的工作原理,神秘的面纱就揭开了。
生活中的一幕。如果你想给朋友打电话,先拨号码,朋友听到电话铃响后会拿起电话,然后你和你的朋友就建立了联系,你们就可以通话了。当沟通结束后,挂断电话,结束对话。生活中的场景解释了这是如何工作的。可能生活中诞生了TCP/IP协议族,也不一定。
图3
让我们从服务器开始。服务器首先初始化套接字,然后绑定到端口,监听端口,调用accept来阻塞,等待客户端连接。此时,如果客户端初始化一个套接字,然后连接到服务器,如果连接成功,则客户端和服务器之间的连接建立。客户端发送数据请求,服务器接收并处理请求,然后向客户端发送响应数据,客户端读取数据,最后关闭连接,一次交互结束。
我们非常清楚信息交流的价值。进程如何在网络中通信?比如我们每天打开浏览器浏览网页时,浏览器进程是如何与web服务器进行通信的?用QQ聊天时,QQ进程如何与服务器或好友所在的QQ进程进行通信?这一切都靠插座?什么是插座?插座有哪些类型?还有socket的基本功能,本文要介绍的。
1、网络中进程之间如何通信?
本地进程间通信(IPC)有多种方式,但可以归纳为以下四类:
消息传递(管道、FIFO、消息队列)同步(互斥、条件变量、读写锁、文件和写记录锁、信号量)共享内存(匿名和命名)远程过程调用(Solaris gate和Sun RPC)
但这些都不是本文的主题!我们要讨论的是网络中进程之间如何通信。首先要解决的问题是如何唯一标识一个进程,否则通信将无法进行!进程可以由进程PID在本地唯一标识,但在网络中不可行。事实上,TCP/ip协议家族已经帮助我们解决了这个问题。网络层的IP地址可以唯一标识网络中的主机,而传输层的协议端口可以唯一标识主机中的应用(进程)。这样,三元组(ip地址、协议、端口)可以用来标识网络中的进程,网络中的进程通信可以使用这个标志与其他进程进行交互。
使用TCP/IP协议的应用通常使用API:UNIX BSD的socket和UNIX System V(已淘汰)的TLI来实现网络进程间的通信。目前几乎所有的应用都采用socket,现在是互联网时代,进程通信无处不在,这也是我说“一切都是socket”的原因。
2、什么是Socket?
我们已经知道网络中的进程通过socket进行通信,那么socket是什么呢?Socket起源于Unix,Unix/Linux的一个基本哲学就是“一切都是文件”,可以在“打开-读写/读-关闭”的模式下操作。我的理解是,socket是这种模式的一种实现,socket是一种特殊的文件,有些Socket函数就是在它上面的操作(读/写IO,打开和关闭)。我们将在后面介绍这些函数。
socket一词的起源
在网络领域的第一次应用是在1970年2月12日发布的IETF RFC33中,作者是斯蒂芬卡尔、史蒂夫克罗克和温顿瑟夫。根据美国计算机历史博物馆的记录,克罗克写道:“命名空间的元素可以被称为套接字接口。一个套接字接口构成一个连接的一端,一个连接完全可以由一对套接字接口来定义。”计算机博物馆补充道:“这比BSD中socket接口的定义早了大约12年。”
3、socket的基本操作
由于socket是“开-写/读-关”模式的实现,所以socket提供了对应这些操作的功能接口。以TCP为例介绍几种基本的socket接口函数。
3.1、socket()函数
int socket(int域,int类型,int协议);
socket函数对应的是普通文件的打开操作。普通的文件打开操作返回一个文件描述符,socket()用来创建一个套接字描述符,唯一标识一个套接字。这个套接字描述符与文件描述符相同,并且在后续操作中使用。把它作为一个参数,通过它可以进行一些读写操作。
就像您可以向fopen传递不同的参数值来打开不同的文件一样。创建套接字时,还可以指定不同的参数来创建不同的套接字描述符。套接字函数的三个参数是:
域:协议域,也称为协议族。常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或AF_Unix、Unix域套接字)、AF_ROUTE等。协议决定了套接字的地址类型,通信时必须使用相应的地址。例如,AF_INET确定ipv4地址(32位)和端口号(16位)的组合,AF_UNIX确定应该使用绝对路径名作为地址。类型:指定套接字类型。常用的套接字类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。(插座有哪些类型?)。协议:顾名思义,就是指定协议。常用的协议有IPPROTO_TCP、IPPTOTO _ UDP、IPPROTO_SCTP、IPPROTO_TIPC等。分别对应TCP传输协议,UDP传输协议,STCP传输协议,TIPC传输协议(这个协议我会单独讨论!)。
注意:不是以上类型和协议可以随意组合,比如SOCK_STREAM不能和IPPROTO_UDP组合。当协议为0时,将自动选择该类型对应的默认协议。
当我们调用socket创建套接字时,返回的套接字描述符存在于地址族(AF_XXX)空间中,但是没有具体的地址。如果要给它分配一个地址,必须调用bind()函数,否则当你调用connect()和listen()时,系统会自动随机分配一个端口。
3.2、bind()函数
如上所述,bind()函数将地址族中的特定地址分配给套接字。比如对应AF_INET和AF_INET6就是给socket分配ipv4或者ipv6地址和端口号的组合。
int bind(int sockfd,const struct sockaddr *addr,socklen _ t addrlen);
该函数的三个参数是:
Sockfd: socket描述符,由socket()函数创建,唯一标识一个套接字。bind()函数将一个名称绑定到这个描述符。Addr:指向要绑定到sockfd的协议地址的const struct sockaddr *指针。根据创建套接字时的地址协议系列,此地址结构是不同的。例如,ipv4对应于:
struct sockaddr_in {
sa _ family _ t sin _ family
in _ port _ t sin _ port
结构in _ addr sin _ 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
};
结构in6_addr {
无符号字符S6 _ addr[16];
};
Unix域对应于:
#定义UNIX路径最大值108
struct sockaddr_un {
sa _ family _ t sun _ family
char sun _ PATH[UNIX _ PATH _ MAX];
};
Addrlen:它对应于地址的长度。
通常服务器启动时会绑定一个众所周知的地址(如ip地址、端口号)提供服务,客户可以通过它连接服务器;但是,客户端不需要指定,系统会自动分配一个端口号和它自己的ip地址的组合。这就是为什么服务器通常在listen之前调用bind(),而客户端不会。相反,系统会在connect()时随机生成一个。
网络字节序与主机字节序
主机端就是我们通常所说的大端和小端模式:不同的CPU有不同的端类型。这些端序是指整数在内存中存储的顺序,称为主机端序。引用的大端序和小端序的定义如下:
A) Little-Endian表示低位字节在存储器的低位地址端放电,高位字节在存储器的高位地址端放电。B) Big-Endian是指高位字节在内存的低位地址端放电,低位字节在内存的高位地址端放电。
网络字节顺序:4个字节的32位值按以下顺序传输:先0 ~ 7位,再8 ~ 15位,再16 ~ 23位,最后24 ~ 31位。这种传输顺序称为大端字节序。因为TCP/IP头中的所有二进制整数都要求在网络中按此顺序传输,所以也叫网络字节序。字节序,顾名思义就是大于一个字节类型的数据在内存中的存储顺序,一个字节的数据是没有顺序的。
因此,在将一个地址绑定到套接字时,请先将主机端序转换为网络端序,不要假设主机端序使用Big-Endian作为网络端序。这个问题造成了血案!因为公司代码中的这个问题,导致很多莫名其妙的问题,所以请记住不要对主机字节顺序做任何假设,一定要转换成网络字节顺序,赋给socket。
3.3、listen()、connect()函数
如果你是服务器,在调用socket()和bind()后会调用listen(),如果客户端调用connect()发出连接请求,服务器会收到这个请求。
int listen(int sockfd,int backlog);
int connect(int sockfd,const struct sockaddr *addr,socklen _ t addrlen);
listen函数的第一个参数是要监控的套接字描述符,第二个参数是相应套接字可以排队的最大连接数。socket()函数创建的socket默认为主动类型,listen函数将socket改为被动类型,等待客户的连接请求。
connect函数的第一个参数是客户端的套接字描述符,第二个参数是服务器的套接字地址,第三个参数是套接字地址的长度。客户端通过调用connect函数与TCP服务器建立连接。
3.4、accept()函数
TCP服务器依次调用socket()、bind()和listen()后,会监听指定的套接字地址。TCP依次调用socket()和connect()后,向TCP服务器发送连接请求。TCP服务器监听到这个请求后,会调用accept()函数来接收请求,从而建立连接。然后就可以开始网络I/O操作了,类似于普通文件的读写I/O操作。
int accept(int sockfd,struct sockaddr *addr,socklen _ t * addrlen);
accept函数的第一个参数是服务器的套接字描述符,第二个参数是指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数是协议地址的长度。如果accpet成功,那么它的返回值就是内核自动生成的一个全新的描述符,表示与返回客户的TCP连接。
注意:accept的第一个参数是服务器的socket描述符,在服务器开始调用socket()函数时生成,称为监控socket描述符;accept函数返回连接的套接字描述符。服务器通常只创建一个监听套接字描述符,该描述符在服务器的生存期内一直存在。内核为服务器进程接受的每个客户机连接创建一个连接套接字描述符。当服务器完成对客户机的服务时,相应的连接套接字描述符被关闭。
3.5、read()、write()等函数
一切只因东风。至此,服务器和客户之间的连接已经建立。可以调用网络I/O读写,即实现网络中不同进程之间的通信!网络I/O操作分为以下几组:
read()/write()recv()/send()readv()/writev()recvmsg()/sendmsg()recvfrom()/send to()
我推荐使用recvmsg()/sendmsg()函数。这两个函数是最通用的I/O函数。事实上,你可以用这两个函数替换上面所有其他的函数。他们的声明如下:
#包括
ssize_t read(int fd,void *buf,size _ t count);
ssize_t write(int fd,const void *buf,size _ t count);
#包括
#包括
ssize_t send(int sockfd,const void *buf,size_t len,int flags);
ssize_t recv(int sockfd,void *buf,size_t len,int flags);
ssize_t sendto(int sockfd,const void *buf,size_t len,int flags,
const struct sockaddr *dest_addr,socklen _ t addrlen);
ssize_t recvfrom(int sockfd,void *buf,size_t len,int flags,
struct sockaddr *src_addr,socklen _ t * addrlen);
ssize_t sendmsg(int sockfd,const struct msghdr *msg,int flags);
ssize_t recvmsg(int sockfd,struct msghdr *msg,int flags);
read函数负责从fd中读取内容。当读取成功时,read返回实际读取的字节数。如果返回值为0,则意味着文件的结尾已被读取。如果它小于0,则意味着发生了错误。如果错误是EINTR,说明读数是中断造成的,如果是ECONNREST,说明网络连接有问题。
write函数将buf中nbytes字节的内容写入文件描述符fd。如果成功,它将返回写入的字节数。失败时返回-1,并设置errno变量。在网络程序中,当我们写入套接字文件描述符时,有两种可能。1)1)write的返回值大于0,表示已经写入了部分或全部数据。2)返回值小于0,此时出现错误。我们必须根据错误的类型来处理它。如果错误为EINTR,则意味着在写入过程中发生了中断错误。如果EPIPE表示网络连接有问题(对方已经关闭连接)。
其他的我就不一一介绍这几对I/O函数了。详情请参考man文档或百度、Google。send/recv将在下面的示例中使用。
3.6、close()函数
服务器和客户端建立连接后,会进行一些读写操作。当读写操作完成后,相应的socket描述符会被关闭,就像操作完打开的文件后调用fclose关闭打开的文件一样。
#包括
int close(int FD);
当关闭TCP套接字的默认行为时,将套接字标记为关闭,然后立即返回到调用进程。该描述符不能再被调用进程使用,也就是说,它不能再被用作read或write的第一个参数。
注意:关闭操作只使对应的套接字描述符的引用计数为-1,只有当引用计数为0时,才会触发TCP客户端向服务器发送连接终止请求。
4、socket中TCP的三次握手建立连接详解
我们知道tcp在建立连接时要“三次握手”,也就是交换三个包。一般流程如下:
向服务器发送一个SYN J。服务器对客户端的SYN K作出响应,并确认SYN J ACK J 1。客户端向服务器发送确认ACK K 1。
只是三次握手,但是套接字函数中的三次握手呢?请看下图:
1.在套接字中发送的TCP三次握手
从图中可以看出,当客户端调用connect时,触发连接请求,向服务器发送SYN J包。此时connect进入阻塞状态;服务器监听连接请求时,接收SYN J包,调用accept函数接收请求,向客户端发送SYN K,ACK J 1。此时,accept进入阻塞状态;客户端收到来自服务器的SYN K,ACK J 1后,然后connect返回并确认SYN K;当服务器收到ACK K 1,accept返回,三次握手完成,连接建立。
总结:在三次握手中,第二次返回客户端的connect,第三次返回服务器的accept。
5、socket中TCP的四次握手释放连接详解
以上介绍了在socket中建立TCP三次握手的过程以及涉及到的socket函数。现在我们来介绍一下socket中四次握手释放连接的过程。请看下图:
图二。在套接字中发送的TCP四次握手
图示的过程如下:
一个应用进程先调用close主动关闭连接,然后TCP发送FINM另一端接收到FIN M后,执行被动关机以确认此FIN。它的接收也作为文件终止符传递给应用进程,因为FIN的接收意味着应用进程不能再在相应的连接上接收额外的数据;一段时间后,接收文件终止符的应用程序进程调用close来关闭它的套接字。这导致其TCP也发送FIN N;接收该FIN的源发送者TCP确认它。
因此在每个方向上都有一个FIN和ACK。
6.下面给出实现的一个实例
先给个实现的截图。
服务器代码如下:
#include 'InitSock.h '
#包含stdio.h
#包括iostream
使用命名空间std
CInitSock initSock//初始化Winsock库
int main()
{
//创建一组节词
SOCKET sListen=:socket(AF_INET,SOCK_STREAM,IP proto _ TCP);
//用于指定套接字使用的地址格式,通常使用AF_INET
//指定套接字的类型。如果是SOCK_DGRAM,则使用udp不可靠传输。
//与type参数一起使用,指定使用的协议类型(指定套接字类型时,可以设置为0,因为默认为UDP或TCP)
if(sListen==INVALID_SOCKET)
{
printf(' Failed socket()\ n ');
返回0;
}
//填充sockaddr_in结构,这是一个结构。
/* struct sockaddr_in {
短sin _ family//地址族(指定地址格式),设置为AF_INET
u _ shortsin _ port//端口号
结构in _ addr sin _ addr//IP地址
char sin _ zero[8];//空的子节,设置为空
} */
sockaddr _ in sin
sin.sin _ family=AF _ INET
sin . sin _ port=htons(4567);//1024 ~ 49151:普通用户注册的端口号
sin.sin_addrS_un。S _ addr=INADDR _ ANY
//将这组单词绑定到本地地址
if(:bind(sListen,(LPSOCKADDR)sin,sizeof(sin))==SOCKET_ERROR)
{
printf('失败的绑定()\ n ');
返回0;
}
//进入监听模式。
//2指的是,监听队列中允许保持的尚未处理的最大连接数
if(:listen(sListen,2)==SOCKET_ERROR)
{
printf(' Failed listen()\ n ');
返回0;
}
//循环接受客户的连接请求
sockaddr _ in远程地址
int nAddrLen=sizeof(远程地址);
套接字s客户端=0;
char szText[]=' TCP服务器演示!\ r \ n ';
while(sClient==0)
{
//接受一个新连接
//((SOCKADDR*)remoteAddr)一个指向sockaddr_in结构的指针,用于获取对方地址
sClient=:accept(sListen,(SOCKADDR*)remoteAddr,nAddrLen);
if(sClient==INVALID_SOCKET)
{
printf(' Failed accept()');
}
printf('接受到一个连接:%s \r\n ',inet _ ntoa(远程地址。sin _ addr));
继续;
}
而(真)
{
//向客户端发送数据
获取(SZ文本);
* send(s client,szText,strlen(szText),0);
//从客户端接收数据
字符缓冲器[256];
int nRecv=:recv(sClient,buff,256,0);
if(nRecv 0)
{
buff[nRecv]=' \ 0 ';
printf('接收到数据:%s\n ',buff);
}
}
//关闭同客户端的连接
*关闭套接字(s客户端);
//关闭监听套节字
*关闭套接字(s listen);
返回0;
}
客户端代码:
#include 'InitSock.h '
#包含标准视频
#包括输入输出流
使用命名空间标准
CInitSock initSock//初始化网络编程接口库
int main()
{
//创建套节字
SOCKET s=:socket(AF_INET,SOCK_STREAM,IP proto _ TCP);
if(s==INVALID_SOCKET)
{
printf(' Failed socket()\ n ');
返回0;
}
//也可以在这里调用约束函数绑定一个本地地址
//否则系统将会自动安排
//填写远程地址信息
sockaddr _ in servAddr
servAddr.sin _ family=AF _ INET
servaddr。sin _ port=htons(4567);
//注意,这里要填写服务器程序(TCP服务器程序)所在机器的互联网协议(互联网协议)地址
//如果你的计算机没有联网,直接使用127.0.0.1即可
servAddr.sin_addr .S_un .s _ addr=inet _ addr(' 127。0 .0 .1 ');
if(:connect(s,(sockaddr*)servAddr,sizeof(servAddr))==-1)
{
printf(' Failed connect()\ n ');
返回0;
}
字符缓冲器[256];
char SZ text[256];
而(真)
{
//从服务器端接收数据
int nRecv=:recv(s,buff,256,0);
if(nRecv 0)
{
buff[nRecv]=' \ 0 ';
printf('接收到数据:%s\n ',buff);
}
//向服务器端发送数据
获取(SZ文本);
SZ text[255]=' \ 0 ';
* send(s,szText,strlen(szText),0);
}
//关闭套节字
*封闭式插座;
返回0;
}
封装的InitSock.h
#包含winsock2.h
#包含标准库
#包含conio.h
#包含标准视频
#杂注注释(lib,' WS2_32') //链接到WS2_32.lib
辛尼索克级
{
公共:
奇尼索克(字节minorVer=2,字节majorVer=2)
{
//初始化WS2_32.dll
WSADATA wsaData
WORD sock版本=make WORD(min orver,major ver);
if(:WSAStartup(sockVersion,wsaData)!=0)
{
退出(0);
}
}
~辛尼索克()
{
* WSACleanup();
}
};
到此这篇关于(电源)插座通信原理和实践的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持我们。
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。