本文详细解释了套接字编程,这是一种在网络中经常使用的技术。本文通过大量代码来解释,大家可以参考一下。
目录
1:Socket概述2: TCP/IP协议3:回过头来了解Socket 4:Socket的一些接口函数原理5:Socket的一个例子,并总结上面的问题6:上面例子中用到的知识点7:下面是一些API函数:
套接字编程是网络中常用的编程方式。我们在网络中创建socket关键字来实现网络间的通信。通过收集大量的资料,我们可以通过本章全面了解socket编程。本文引用了大量大神的分析,加上我们自己的理解,做一篇总结性的文章。
1:socket大致介绍
套接字编程是一种技术,在网络通信中经常使用。
既然是技术,现在又是面向对象编程,计算机界的一些大神通过抽象的思路和现实中反复的理论或实践推导,提出了一些基于tcp/ip协议的抽象通信协议,提出了一些通用的思路。在这个协议的基础上,一些通用程序已经接口了这些抽象的概念。对于协议提出的每一个想法,都有专门编写的接口与其协议一一对应,形成了现在的socket。
目前开发者已经开发了很多封装类来完善socket编程,都是为了更方便的在一开始就实现socket通信的所有环节,所以我们首先要了解socket的通信原理。只有从本质上了解了socket的通信,才能快速便捷地了解socket的所有环节,真正从底层把握。
2:TCP/IP协议
要理解socket,就要理解tcp/ip,这就好比信差线和驿站的作用。比如建议一个messenger站,就要知道messenger的所有细节。
TCP/IP协议不同于iso的七层协议。就是按照这七层划分的,比如保洁。最初有扫帚、垃圾桶、抹布、油漆、盆栽等。这就像OSI的标准层。tcp/ip根据用途和功能将扫帚和垃圾桶放在粗整理层,抹布和油漆放在中间整理层,盆栽放在最终效果层。这里,TCP/IP也对OSI的网络模型层进行了划分:大致如下:
OSI模型:
TCP/IP协议参考模型将所有TCP/IP协议分为四个抽象层。
应用层:TFTP、HTTP、SNMP、FTP、SMTP、DNS、Telnet等
传输层:TCP、UDP
网络层:IP,ICMP,OSPF,IGMP
数据链路层:SLIP、CSLIP、PPP、MTU
每个抽象层都是基于较低层提供的服务,为较高层提供服务,看起来是这样的。
根据上图,由于下层需要向上层提供服务,我们大致理解应用需要传输层的tcp和网络层的ip协议来提供服务,但是我们本章要分析的socket是tcpip协议的哪一部分呢?比如我们的沟通线路已经明确,我们的岗位应该设计在哪里?
3:回过头再来理解socket
至此,我们对应用程序与tcpip协议的关系有了一个大致的了解。我们只知道socket编程是tcp/IP上的网络编程,但是上面模型中socket在哪里呢?这个职位是由一个天才理论家或者一个抽象的计算机大神提出并安排的。
我们可以发现socket是在应用的传输层和应用层之间,设计了一个socket抽象层。传输层底层的服务被提供给套接字抽象层,套接字抽象层又被提供给应用层。问题又出现了。应用层如何与套接字抽象层通信,传输层与网络层之间如何通信?在知道这些之前,我们最好回到原点。
要了解socket编程如何通过socket关键字实现服务器和客户端的通信,就要了解tcp/ip是如何通信的,在此基础上还要了解socket的握手通信。
在tcp/ip协议中,tcp通过三次握手建立tcp链路,大致如下
第一次握手:客户端尝试连接服务器,向服务器发送一个syn包,syn=j,客户端进入SYN_SEND状态等待服务器的确认。
第二次握手:服务器收到客户端的syn包并确认(ack=j 1),向客户端发送一个SYN包(syn=k),即SYN ACK包。此时,服务器进入SYN_RECV状态。
三次握手:客户端从服务器接收SYN ACK包,并向服务器发送确认包ACK(ack=k 1)。这个包发出后,客户端和服务器进入建立状态,三次握手完成。
三次握手如下:
根据tcp的三次握手,socket也定义了三次握手,或许指的就是tcp的三次握手。有电脑大神画过socket三次握手的模型图。
模型图如下:
在上图的基础上,如果得到上图,需要自己开发一些接口来满足上面通信的三方握手,问题就出来了。我们需要开发什么功能?
4:socket的一些接口函数原理
通过上面的图,我们知道我们就像一些通用的程序员,一些理论提供者给我们提供了上面的图论。我们需要做的是把上图的抽象的东西具体化。
第一次握手:客户端需要发送一个syn j包,尝试链接服务器,所以我们需要为客户端提供一个链接功能。
第二次握手:服务器需要接收客户端发送的syn J 1包,然后发送ack包,所以我们需要有服务器端的接受处理功能。
三次握手:客户端的处理功能和服务器的处理功能。
三次握手只是一个数据传输的过程。但是我们在传输之前需要做一些准备工作,比如创建一个socket,收集一些计算机资源,将一些资源绑定到socket中,以及接收和发送数据的函数。这些功能接口一起构成了套接字编程。
以下功能按照客户端和服务器分别详细列出。
上面两张图概括了socket的通信原理。
5:socket的一个例子,总结上述的问题
我就不详细解释了,通过一段代码详细解释一下。
客户代码:
#包含winsock2.h
#包含stdio.h
#pragma注释(lib, ws2_32.lib )
int main()
{
//在socket之前进行一些检查,检查协议库的版本,以避免其他版本的socket,并传递
//WSAStartup启动相应的版本。WSAStartup的一个参数是版本信息,另一个是一些详细的细节。注意高低位。
//WSAStartup对应WSACleanup
int err
需要WORD版本;
WSADATA wsaData
versionRequired=MAKEWORD(1,1);
err=WSAStartup(versionRequired,wsa data);//协议库的版本信息
//通过WSACleanup的返回值判断套接字协议是否启动。
如果(!呃)
{
Printf(客户端嵌套word已经打开! n’);
}
其他
{
打开printf (client的嵌套单词失败! n’);
返回0;//结束
}
//创建关键字socket。这里,考虑一下图中的套接字抽象层。
//注意socket函数。它的三个参数定义了套接字的系统、套接字的类型和其他一些信息。
SOCKET client SOCKET=SOCKET(AF _ INET,SOCK_STREAM,0);
//在socket编程中,定义了一个结构SOCKADDR_IN来存储计算机的一些信息,像socket的体系,
//端口号、ip地址等信息。其中存储了服务器端计算机的信息。
SOCKADDR _ IN clientsock _ in
clientsock_in.sin_addr。S_un。s _ addr=inet _ addr( 127 . 0 . 0 . 1 );
client sock _ in . sin _ family=AF _ INET;
client sock _ in . sin _ port=htons(6000);
//Socket是前期定义的,服务器端的计算机的一些信息存储在clientsock_in中,
//准备工作完成后,再开始链接这个套接字到远程计算机。
//也就是第一次握手
connect(clientSocket,(SOCKADDR*)clientsock_in,sizeof(SOCKADDR));//启动连接
char receiveBuf[100];
//解释套接字的内容
recv(clientSocket,receiveBuf,101,0);
printf(%sn ,receive buf);
//发送套接字数据
send(clientSocket,你好,这是客户端,strlen(你好,这是客户端)1,0);
//关闭套接字
close socket(client socket);
//关闭服务
WSACleanup();
返回0;
}
对应服务器的代码
#包含winsock2.h
#包含stdio.h
#pragma注释(lib, ws2_32.lib )
int main()
{
//创建一个socket,socket之前的一些检查工作,包括服务的启动。
WORD myVersionRequest
WSADATA wsaData
myVersionRequest=MAKEWORD(1,1);
int err
err=wsa startup(myVersionRequest,wsa data);
如果(!呃)
{
Printf(套接字已打开 n );
}
其他
{
//进一步绑定套接字
Printf(嵌套单词不打开!);
返回0;
}
SOCKET serSocket=socket(AF_INET,SOCK_STREAM,0);//创建了可识别的套接字。
//要绑定的参数,主要是本地socket的一些信息。
SOCKADDR _ IN addr
addr.sin _ family=AF _ INET
addr.sin_addr。S_un。s _ addr=htonl(in addr _ ANY);//ip地址
addr . sin _ port=htons(6000);//绑定端口
bind(serSocket,(SOCKADDR*)addr,sizeof(SOCKADDR));//绑定完成
listen(serSocket,5);//其中第二个参数表示可以接收的最大连接数
SOCKADDR _ IN clientsocket
int len=sizeof(SOCKADDR);
while (1)
{
//第二次握手,通过accept接受对方的套接字信息。
SOCKET serConn=accept(serSocket,(SOCKADDR*)clientsocket,len);//如果这不是accept而是conection。会继续监控。
char send buf[100];
sprintf(sendBuf,欢迎%s来北京,inet _ ntoa(client socket . sin _ addr));//找到相应的IP,在那里打印这一行
//发送信息
send(serConn,sendBuf,strlen(sendBuf) 1,0);
char receiveBuf[100];//接收
recv(serConn,receiveBuf,strlen(receiveBuf) 1,0);
printf(%sn ,receive buf);
close socket(serConn);//关闭
WSACleanup();//释放资源的操作
}
返回0;
}
6:上面例子用到的知识点
(摘自卡特大神文章):
服务器端:
流程如下:首先,服务器启动,根据请求提供相应的服务:
(1)打开一个通信信道,通知本地主机它愿意在一个可识别的地址上的端口接收客户请求(例如,FTP的端口可能是21);
(2)等待客户要求到达港口;
(3)当接收到来自客户端的服务请求时,处理该请求并发送响应信号。当接收到并发服务请求时,应该激活一个新的进程来处理客户端请求(比如UNIX系统中的fork和exec)。新流程处理这个客户请求,不需要响应其他请求。服务完成后,关闭这个新流程与客户之间的通信链接,并终止它。
(4)返回步骤(2)并等待另一个客户请求。
(5)关闭服务器。
客户:
(1)打开一个通信通道,连接到服务器所在主机的特定端口;
(2)向服务器发送服务请求消息,等待并接收响应;继续提要求。
(3)请求结束后,关闭通信通道并终止。
从上述过程可以看出:
(1)客户端和服务器进程的角色是不对称的,所以代码是不同的。
(2)服务器进程一般先启动。只要系统运行,服务进程就会存在,直到被正常终止或强制终止。
7:下面就介绍一些API函数:
(摘自卡特大神文章):
创建套接字socket()
在应用程序使用套接字之前,它必须首先有一个套接字。系统调用socket()为应用程序提供创建套接字的方法,其调用格式如下:
SOCKET PASCAL FAR socket(int af,int type,int protocol)
这个调用接收三个参数:af、类型和协议。af参数指定了通信发生的区域:AF_UNIX、AF_INET、AF_NS等。但是DOS和WINDOWS只支持AF_INET,这是互联网领域。因此,地址族与协议族相同。type参数描述了要建立的套接字的类型。这里有三种类型:
(1)首先,TCP流套接字(SOCK_STREAM)提供了一种面向连接的可靠的数据传输服务。数据无错误、无重复地发送,并按发送顺序接收。内置流量控制,避免数据流量超限;数据视为字节流,没有长度限制。文件传输协议(FTP)使用流式套接字。
(2)第二,数据报套接字(SOCK_DGRAM)提供无连接服务。数据包是作为独立的包发送的,没有无错保证,数据可能会丢失或重复,接收顺序也是乱序的。网络文件系统(NFS)使用数据报套接字。
(3)原语套接字(SOCK_RAW)该接口允许直接访问较低层的协议,如IP和ICMP。它通常用于验证新协议的实施或访问现有服务中配置的新设备。
协议参数指示套接字使用的特定协议。如果调用者不想指定具体使用的协议,则将其设置为0,并使用默认连接模式。根据这三个参数,建立一个套接字,给它分配相应的资源,返回一个整数套接字。因此,socket()系统调用实际上指定了相关五元组中的元素“协议”。
指定本地地址bind()
用socket()创建套接字时,有一个命名空间(地址族),但没有命名。Bind()将套接字地址(包括本地主机地址和本地端口地址)与创建的套接字编号相关联,即给套接字起名字以指定本地半相关。呼叫格式如下:
int PASCAL FAR bind(SOCKET s,const struct sockaddr FAR * name,int name len);
参数s是socket()调用返回的未连接的套接字描述符(套接字编号)。参数名称是分配给套接字s的本地地址(名称),其长度是可变的,并且其结构随着不同的通信域而变化。Namelen表示名称的长度。如果没有错误发生,bind()返回0。否则,返回SOCKET_ERROR。
建立套接字连接connect()与accept()
这两个系统调用用于完成一个完整的相关建立,其中connect()用于建立连接。Accept()用于让服务器等待来自客户端进程的实际连接。
connect()的调用格式如下:
int PASCAL FAR connect(SOCKET s,const struct sockaddr FAR * name,int name len);
s参数是要建立的连接的本地套接字描述符。name参数表示描述对方套接字地址结构的指针。套接字地址的长度由namelen指定。
如果没有发生错误,connect()返回0。否则,将返回SOCKET_ERROR值。在面向连接的协议中,这个调用导致本地系统和外部系统之间连接的实际建立。
因为地址族总是包含在socket地址结构的前两个字节中,并且通过socket()调用与一个协议族相关联。因此,bind()和connect()不需要协议作为参数。
accept()的调用格式如下:
SOCKET PASCAL FAR accept(SOCKET s,struct sockaddr FAR* addr,int FAR * addrlen);
参数s是本地套接字描述符,在用作accept()调用的参数之前,应该先调用listen()。指向客户端套接字地址结构的Addr指针,用于接收连接实体的地址。addr的确切格式由创建套接字时建立的地址族决定。Addrlen是客户端套接字地址的长度(以字节为单位)。如果没有错误发生,accept()返回一个SOCKET type值,该值表示接收到的套接字的描述符。否则,返回值为INVALID_SOCKET。
Accept()用于面向连接的服务器。参数addr和addrlen存储客户端的地址信息。调用前,参数addr指向一个初始值为空的地址结构,而addrlen的初始值为0;调用accept()后,服务器等待从编号为S的套接字接受客户端的连接请求,连接请求由客户端的connect()调用发出。当一个连接请求到达时,accept()调用将请求连接队列上第一个客户机套接字的地址和长度放入addr和addrlen,并创建一个与s具有相同特征的新套接字号,新套接字可用于处理服务器并发请求。
四个socket系统调用,socket(),bind(),connect()和accept(),可以完成一个完整的五元素关联。Socket()指定了五元组中的协议元素,它的用法与它是客户端还是服务器,是不是面向连接无关。Bind()指定五元组中的本地二进制,即本地主机地址和端口号。其用法与是否面向连接有关:在服务器端,无论是否面向连接,都必须调用bind()。如果是面向连接的,可以通过connect()自动完成,不需要调用bind()。如果采用无连接,客户端必须使用bind()来获得唯一的地址。
监听连接listen()
此调用用于面向连接的服务器,表示它愿意接收连接。在accept()之前需要调用Listen(),其调用格式如下:
int PASCAL远听(SOCKET s,int backlog);
s参数标识已经在本地建立但尚未连接的套接字,服务器愿意接收来自它的请求。Backlog表示请求连接队列的最大长度,用于限制排队请求的数量。目前,最大允许值为5。如果没有错误发生,listen()返回0。否则,它返回SOCKET_ERROR。
Listen()可以为调用过程中未调用bind()的套接字完成必要的连接,建立一个backlog长度的请求连接队列。
调用listen()是服务器接收连接请求的四个步骤中的第三步。在调用socket()分配流套接字和调用bind()给S取名字之后调用,必须在accept()之前调用。
数据传输send()与recv()
连接建立后,就可以传输数据了。常用的系统调用有send()和recv()。
send()调用用于在S指定的连接数据报或流套接字上发送输出数据,格式如下:
int PASCAL FAR send(SOCKET s,const char FAR *buf,int len,int flags);
s参数是连接的本地套接字描述符。Buf是一个指针,指向存储待发送数据的缓冲区,其长度由len指定。Flags指定传输控制模式,例如是否发送带外数据。如果没有发生错误,send()返回发送的总字节数。否则,它返回SOCKET_ERROR。
Recv()调用用于在S指定的连接数据报或流套接字上接收输入数据,格式如下:
int PASCAL FAR recv(SOCKET s,char FAR *buf,int len,int flags);
参数s是连接的套接字描述符。指向接收输入数据的缓冲区的指针,其长度由len指定。Flags指定了传输控制模式,如是否接收带外数据等。如果没有错误发生,recv()返回接收的总字节数。如果连接关闭,则返回0。否则,它返回SOCKET_ERROR。
输入/输出多路复用select()
select()调用用于检测一个或多个套接字的状态。对于每个套接字,该调用可以请求关于读、写或错误状态的信息。请求给定状态的套接字集由fd_set结构指示。返回时,该结构被更新以反映满足特定条件的套接字子集。同时,select()调用返回满足条件的套接字数量,其调用格式如下:
int PASCAL FAR select(int nfds,fd_set FAR * readfds,fd_set FAR * writefds,fd_set FAR * exceptfds,const struct time val FAR * time out);
参数nfds表示要检查的套接字描述符的值范围,这个变量通常被忽略。
参数readfds指向要读取的套接字描述符集,调用者希望从中读取数据。参数writefds是指向要写入的套接字描述符集的指针。指向要检测错误的套接字描述符集的指针。Timeout指向select()函数的最大等待时间。如果设置为NULL,则是阻塞操作。Select()返回fd_set结构中包含的准备好的套接字描述符的总数,如果出现错误,则返回SOCKET_ERROR。
关闭套接字closesocket()
Closesocket()关闭套接字并释放分配给套接字的资源;如果s涉及一个开放的TCP连接,则释放该连接。closesocket()的调用格式如下:
BOOL PASCAL FAR close SOCKET(SOCKET s);
要关闭的参数套接字描述符。如果没有发生错误,closesocket()返回0。否则,将返回SOCKET_ERROR值。
以上是socket编程的详细说明。关于套接字编程的更多信息,请关注我们的其他相关文章!