一、学习Socket通信的原理
Socket通信的原理是什么呢?看下图1:
(图1)
图1是socket的server-client通信模式图。
第一个要理解的概念:
套接字(Socket):
多个TCP连接或多个应用程序进程可能需要通过同一个 TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了称为套接字(Socket)的接口。
应用程序和应用程序之间的通信就是通过socket(套接字)来交换信息的。怎么交换信息呢?
比如说应用程序1要和应用程序3进行通信,那么socket就起到了桥梁的作用了,每一个socket都有它自己的输入流和输出流,我们这里就简单理解为向网络发送数据信息和从网络接收数据信息的函数吧,两函数:send和 recv ,就是发送和接收函数。而服务器要有应用程序1和应用程序3的socket,分别是:socket1和socket3。
通信原理如下:
应用程序1用自己的函数send向服务器发送信息,说我要和应用程序3进行通信,我要对他说:“你吃饭了吗?”,服务器用应用程序1的函数 recv接收应用程序1的发来的信息,然后服务器根据信息找到应用程序3的send函数,向应用程序3发送信息,说“你吃饭了吗”,应用程序3用自己的函数recv进行接收,收到了“你吃饭了吗”这样的信息,然后做出回答:“我吃了”,用自己的函数send发给服务器,服务器就用他的recv函数接收并用应用程序1的send函数发给应用程序1,应用程序1用自己的recv函数接收,现实了信息交流。这里服务器就相当于一个交换器,通过使用socket实现信息交换的功能。但有一点是,通信之前必须建立连接。
所谓的套接字,是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
Socket可以看成在两个程序进行通讯连接中的一个端点,是连接应用程序和网络驱动程序的桥梁,Socket在应用程序中创建,通过绑定与网络驱动建立关系。此后,应用程序送给Socket的数据,由Socket交给网络驱动程序向网络上发送出去。计算机从网络上收到与该Socket绑定IP地址和端口号相关的数据后,由网络驱动程序交给Socket,应用程序便可从该Socket中提取接收到的数据,网络应用程序就是这样通过Socket进行数据的发送与接收的。
二、学习实现socket的server-client通信模式
*服务器端的步骤如下(参考图1):
(1)建立服务器的socket和初始化socket
怎么建立呢?使用socket()函数现实,那么这函数怎么使用,求助一些资料,了解了函数socket,如下:
socket()函数用于根据指定的地址族、数据类型和协议来分配一个套接口的描述字及其所用的资源。
函数原型:
SOCKET PASCAL FAR socket( int af, int type, int protocol); 包含的头文件:#include
1、参数af取值是:AF_INET、AF_INET6、AF_UNIX
AF_INET:
这是大多数用来产生socket的协议,使用TCP或UDP来传输,用在IPv4的地址。
AF_INET6:
与上面类似,不过是来用在IPv6的地址。
AF_UNIX :
本地协议,使用在Unix和Linux系统上,它很少使用,一般都是当客户端和服务器在同一台及其上的时候使用
2、参数type的取值:
type:指定socket类型。新套接口的类型描述类型,如TCP(SOCK_STREAM)和UDP(SOCK_DGRAM)。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等。
SOCK_STREAM:
提供有序的、可靠的、双向的和基于连接的字节流,使用带外数据传送机制,为Internet地址族使用TCP。SOCK_STREAM类型的套接口为全双向的字节流。对于流类套接口,在接收或发送数据前必需处于已连接状态。用connect()调用建立与另一套接口的连接,连接成功后,即可用send()和recv()传送数据。当会话结束后,调用closesocket()。实现SOCK_STREAM类型套接口的通讯协议保证数据不会丢失也不会重复。如果终端协议有缓冲区空间,且数据不能在一定时间成功发送,则认为连接中断,其后续的调用也将以WSAETIMEOUT错误返回。
SOCK_DGRAM:
支持无连接的、不可靠的和使用固定大小(通常很小)缓冲区的数据报服务,为Internet地址族使用UDP。
SOCK_SEQPACKET:
这个协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
SOCK_RAW:
这个socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)
SOCK_RDM:
这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序。
3、参数protocol
指定协议。套接口所用的协议。如调用者不想指定,可用0。常用的协议有,IPPROTO_TCP、IPPROTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
选好参数,创建如
SOCKET sockServer; // 服务端 Socket
sockServer = socket(AF_INET, SOCK_STREAM, 0); // 创建服务器端socket
服务端socket创建完后,但是每一个有意义的socket都我一个唯一的地址,所以接下来的一步是给socket定义地址,此地址类型是SOCKADDR_IN结构,其中的参数有如:服务端的ip、端口、协议类型。怎么设置这些参数呢?代码如下:
SOCKADDR_IN addrServer;// 服务端地址
addrServer.sin_addr.S_un.S_addr = htonl(INADDR_ANY); // 本机IP
addrServer.sin_family = AF_INET; // 协议类型是INET
addrServer.sin_port = htons(6000); // 绑定端口6000
虽然socket的创建和地址有了,但是socket的地址和socket的没有关系,这时我们要用一个函数实现相关,此函数是:bind();
// 将服务器端socket绑定在本地端口
bind(sockServer, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));
执行这函数的意义是使地址结构addrServer和sockServe相关。这样我们创建的服务器的socket就有了地址了。
为了在应用程序当中调用任何一个Winsock API函数,首先第一件事情就是必须通过WSAStartup函数完成对Winsock服务的初始化,因此需要调用WSAStartup函数。使用Socket的程序在使用Socket之前必须调用WSAStartup函数。接下来我们学习socket的初始化。
4、使用函数WSAStartup初始化socket
函数的原型是:
int WSAStartup ( WORD wVersionRequested, LPWSADATA lpWSAData );
此函数的第一个参数指明程序请求使用的Socket版本,其中高位字节指明副版本、低位字节指明主版本;操作系统利用第二个参数返回请求的Socket的版本信息。当一个应用程序调用WSAStartup函数时,操作系统根据请求的Socket版本来搜索相应的Socket库,然后绑定找到的Socket库到该应用程序中。以后应用程序就可以调用所请求的Socket库中的其它Socket函数了。
⑴ wVersionRequested:一个WORD(双字节)型数值,在最高版本的Windows Sockets支持调用者使用,高阶字节指定小版本(修订本)号,低位字节指定主版本号。
⑵lpWSAData 指向WSADATA数据结构的指针,用来接收Windows Sockets[1]实现的细节。
WindowsSockets API提供的调用方可使用的最高版本号。高位字节指出副版本(修正)号,低位字节指明主版本号。
初始化socket的相应代码如下:
//两参数的定义
int err; // 错误信息
WSADATA wsaData; // winsock 结构体
WORD wVersionRequested;// winsock 的版本
wVersionRequested = MAKEWORD( 2, 2 );// 配置 Windows Socket版本
err=WSAStartup(wVersionRequested,&wsaData);//初始化 Windows Socket
还有错误检测代码:
if ( err != 0 )
{
return; // 启动错误,程序结束
}
if ( LOBYTE(wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 2 )
{
// 启动错误,程序结束
WSACleanup(); // 终止Winsock 2 DLL (Ws2_32.dll) 的使用
return;
}
(2)开始监听整个网络的连接请求
Socket创建后,开始监听请求,在这使用函数listen()实现。所以我要学习函数listen()用法。
#include
int PASCAL FAR listen( SOCKET s, int backlog);
listen作用是:创建一个套接口并监听申请的连接.
S:用于标识一个已捆绑未连接套接口的描述字。
backlog:等待连接队列的最大长度。
// Listen 监听端口
listen(sockServer, 5); // 5 为等待连接数目
监听端口,等待请求
(3)接受请求建立连接
函数accept
SOCKET accept( SOCKET s, struct sockaddr FAR *addr, int FAR *addrlen ); 服务程序调用accept函数从处于监听状态的流套接字s的客户连接请求队列中取出排在最前的一个客户请求,并且创建一个新的套接字来与客户套接字创建连接通道,如果连接成功,就返回新创建的套接字的描述符,以后与客户套接字交换数据的是新创建的套接字;如果失败就返回 INVALID_SOCKET。accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字。
SOCKET sockClient; // 客户端 Scoket
SOCKADDR_IN addrClient;// 客户端地址
sockClient = accept(sockServer, (SOCKADDR *)&addrClient, &len);
此时有两个套接字,一个是服务器的套接字sockServer,另一个是服务器从流套接字sockServer的客户连接请求队列取出并使用函数 accept创建的套接字sockClient,也就是服务器通过套接字sockClient和客户端进行通信的。
这一步完成后服务器端和客户端就建立连接了。接下来我们看看服务端和客户端是怎么通信的。
(4)服务端和客户端的通信实现
1、服务端向客户端发出信息
函数send()用于向一个已经连接的socket发送数据,如果无错误,返回值为所发送数据的总数,否则返回SOCKET_ERROR。
#include
int PASCAL FAR send( SOCKET s, const char FAR* buf, int len, int flags);
s:一个用于标识已连接套接口的描述字。
buf:包含待发送数据的缓冲区。
len:缓冲区中数据的长度。
flags:调用执行方式。
比如:
sprintf(sendBuf, "hello client\n");
send(sockClient, sendBuf, strlen(sendBuf) + 1, 0);//向客户端发送字符串
sockClient:是发送的对象,是客户端的套接字。
sendBuf:发送的信息。
strlen(sendBuf) + 1:发送信息的长度。
0:发送方式。
2、服务端接收客户端返回的信息
使用函数recv(sockClient, recvBuf, 100, 0);实现。
客户端接收服务端发现的信息也是使用此函数完成,所以说客户端相当于用自己的send函数和recv函数完成与服务器的交流。
Recv函数的使用和send一样。
(5)关闭socket
通信完成就要关闭连接, closesocket(sockClient);
*客户端的步骤如下(参考图1):
(1)建立客户端的socket与初始化
和服务器的socket建立一样,我们使用函数socket();确定3参数和服务器的创建socket时的参数一样。
SOCKET sockClient; // 客户端 Scoket
sockClient = socket(AF_INET, SOCK_STREAM, 0);// 新建客户端 scoket
初始化socket:(不过初始化应该写在前面)
Int err;
WSADATA wsaData;
WORD wVersionRequested;
wVersionRequested = MAKEWORD( 2, 2 );
err = WSAStartup( wVersionRequested, &wsaData );
错误检测:
if ( err != 0 )
{
return;
}
if ( LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 2 )
{ // 启动错误,程序结束
WSACleanup( );
return;
}
设置socket的服务端地址参数:
SOCKADDR_IN addrServer; // 服务端地址
// 定义要连接的服务端地址
addrServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
// 目标IP (127.0.0.1是本机地址)
addrServer.sin_family = AF_INET;
// 协议类型是INET
addrServer.sin_port = htons(6000);
// 连接端口1234
注:为什么是127.0.0.1,因为我们绑定的服务器地址是本机。协议和端口和服务器设置的一样。这有才能连接上服务器。接下来连接服务器。
(2)连接服务器
使用函数connect();实现。
int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
connect()用来将参数sockfd 的socket 连至参数serv_addr 指定的网络地址。结构sockaddr请参考bind()。参数addrlen为sockaddr的结构长度。
参数一:套接字描述符
参数二:指向数据机构sockaddr的指针,其中包括目的端口和IP地址
参数三:参数二sockaddr的长度,可以通过sizeof(struct sockaddr)获得
SOCKET sockClient; // 客户端 Scoket
SOCKADDR_IN addrServer; // 服务端地址
// 让 sockClient 连接到 服务端
connect(sockClient, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));
(3)接收和发送信息
接收服务器的信息:
// 从服务端获取数据
recv(sockClient, recvBuf, 100, 0);
给服务器发送信息:
message = "hello server";
// 发送数据到服务端
send(sockClient, message, strlen(message) + 1, 0);
(4)关闭socket
通信完成就要关闭连接, closesocket(sockClient);
三、完整的socket的server-client模式实现代码
服务器:
#include
#include
#pragma comment(lib, "ws2_32.lib")
void main()
{
int err; // 错误信息
int len;
char sendBuf[100]; // 发送至客户端的字符串
char recvBuf[100]; // 接受客户端返回的字符串
SOCKET sockServer; // 服务端 Socket
SOCKADDR_IN addrServer;// 服务端地址
SOCKET sockClient; // 客户端 Scoket
SOCKADDR_IN addrClient;// 客户端地址
WSADATA wsaData; // winsock 结构体
WORD wVersionRequested;// winsock 的版本
// 配置 Windows Socket版本
wVersionRequested = MAKEWORD( 2, 2 );
// 初始化 Windows Socket
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 )
{
// 启动错误,程序结束
return;
}
/*
* 确认WinSock DLL支持2.2
* 请注意如果DLL支持的版本大于2.2至2.2
* 它仍然会返回wVersion2.2的版本
*/
if ( LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 2 )
{
// 启动错误,程序结束
WSACleanup(); // 终止Winsock 2 DLL (Ws2_32.dll) 的使用
return;
}
// 定义服务器端socket
sockServer = socket(AF_INET, SOCK_STREAM, 0);
// 设置服务端 socket
addrServer.sin_addr.S_un.S_addr = htonl(INADDR_ANY); // 本机IP
addrServer.sin_family = AF_INET; // 协议类型是INET
addrServer.sin_port = htons(6000); // 绑定端口6000
// 将服务器端socket绑定在本地端口
bind(sockServer, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));
// Listen 监听端口
listen(sockServer, 5); // 5 为等待连接数目
printf("服务器已启动:\n监听中...\n");
len = sizeof(SOCKADDR);
while (1)
{
// accept 会阻塞进程,直到有客户端连接上来为止
sockClient = accept(sockServer, (SOCKADDR *)&addrClient, &len);
// 当客户端连接上来时, 拼接如下字符串
sprintf(sendBuf, "hello client\n");
// 向客户端发送字符串
send(sockClient, sendBuf, strlen(sendBuf) + 1, 0);
// 获取客户端返回的数据
recv(sockClient, recvBuf, 100, 0);
// 打印客户端返回的数据
printf("%s\n", recvBuf);
// 关闭socket
closesocket(sockClient);
}
}
客户端:
#include
#include
#pragma comment(lib, "ws2_32.lib")
void main()
{
int err;
char *message;
char recvBuf[100];
SOCKET sockClient; // 客户端 Scoket
SOCKADDR_IN addrServer; // 服务端地址
WSADATA wsaData;
WORD wVersionRequested;
wVersionRequested = MAKEWORD( 2, 2 );
err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 )
{
return;
}
if ( LOBYTE( wsaData.wVersion ) != 2 || HIBYTE( wsaData.wVersion ) != 2 )
{
// 启动错误,程序结束
WSACleanup( );
return;
}
// 新建客户端 scoket
sockClient = socket(AF_INET, SOCK_STREAM, 0);
// 定义要连接的服务端地址
addrServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); // 目标IP (127.0.0.1是本机地址)
addrServer.sin_family = AF_INET; // 协议类型是INET
addrServer.sin_port = htons(6000); // 连接端口1234
// 让 sockClient 连接到 服务端
connect(sockClient, (SOCKADDR *)&addrServer, sizeof(SOCKADDR));
// 从服务端获取数据
recv(sockClient, recvBuf, 100, 0);
// 打印数据
printf("%s\n", recvBuf);
message = "hello server";
// 发送数据到服务端
send(sockClient, message, strlen(message) + 1, 0);
// 关闭socket
closesocket(sockClient);
WSACleanup();
getchar(); // 暂停
}
执行如下: