网络上的两个程序t通信通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。对于一个网络连接来说,套接字是平等的,并没有差别,不因为在服务器端或在客户端而产生不同级别。
使用套接字通信的有TCP和UDP两种协议。两种协议共用基本的socket函数接口。
首先说明TCP使用socket函数通信的过程。
TCP套接字之间的连接过程可以分为三个步骤:服务器监听,客户端请求,连接确认。
TCP通信时,必须进行以下三步:
(1)服务器监听。服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。(服务器端使用listen函数将服务器置于监听状态,listen函数要与accept函数一起使用。)
(2)客户端请求。是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后才向服务器端套接字提出连接请求。(客户端必须使用connect函数来连接服务器)。
(3)连接确认。当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,连接就建立好了。此后服务器端套接字继续处于监听状态,接收其他客户端套接字的连接请求。(这一步使用到了accept函数进行处理,实际上accept是TCP三次握手的核心,与connect函数共同完成了三次握手中的重要部分)。
下面针对TCP、UDP中使用的socket函数进行说明。
(1)socket函数。
TCP和UDP都得使用socket函数来创建套接字。
socket函数用于新建套接字,新建好的套接字端口号和IP地址都是空的。TCP和UDP无论服务器端还是客户端,都必须使用到此函数。如何绑定到具体的端口和IP地址请参考 后文bind函数。
第一个参数:代表协议族。一般选择AF_INET,代表是IPv4协议。
第二个参数:socket类型。一般选择SOCK_STREAM: TCP协议,SOCK_DGRAM:UDP。
最后一个参 数。一般是0。0时会自动选择第二个参数类型对应的默认协议。如果不是0,那么第二个参数和第三个参数选择时要对应起来,如SOCK_STREAM时要选择 IPPROTO_TCP,SOCK_DGRAM,IPPROTO_UDP。不可以乱选,造成不匹配。
(2)bind函数。
TCP和UDP都必须在服务器端使用到此函数,来指明套接字的本地地址(本地端口号+本地IP地址)。
TCP和UDP,在客户端可以选用此函数,但是客户端一般无需绑定端口,因为客户端不关心自己的端口号是多少,如果不调用bind,操作系统内核会自动分配一个端口。如 果确实需要一个确定端口号,则也须采用此函数绑定端口号。
服务器端作为被动连接的一方,根据TCP/IP通信中 “IPAddress+端口号”的通信原理,服务器端必须将服务器端套接字与端口绑定绑定,同时将绑定的端口和本地IP告知客户 端。鉴于此函数参数容易理解,不多解释。
(3)connect函数。
TCP必用,UDP可选用。connect函数是客户端使用的连接服务器的函数。一旦与远程服务器主机取得连接,此后就不必在每次发送数据时都指定远程主机指定目的地址。
在UDP通信中,服务器端不使用listen将套接字置于侦听状态。UDP通信中客户端可以不用取得连接后在发送数据,因此UDP协议的客户端可以不用connect。在UDP通信中,发送数据时可以采用sendto函数指定目的地址。后续会专门提到sendto 、recvfrom、send、recv区别。
由于TCP是面向连接的通信,所以必须使用connect用于和服务器端取得联系。TCP通信中,客户端执行了connect后,客户端的socket就有了连接对象(与服务器中的侦听套接字连接),至此,套接字始终与服务器监听套接字保持连接状态,直到对方accept处理完成了这个连接,并在服务器端将连接对象转向新建的套接字(操作系统完成,用户看不到)。
(4)listen函数。
listen是TCP协议中服务器端必须使用的函数。由于UDP协议采用无连接的服务,所以不会使用到此函数。 在TCP服务器中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。 listen函数一般在调用bind之后 , 调用accept之前调用。
listen函数有两参数。第一个参数代表一个已绑定但未被客户端连接的套接字描述符。第二个参数是等待连接队列的最大长度,可以认为开启的队列,可以同时处理的连接数目,一旦连接处理成功,就会从队列中移除。如果你将参数定为10, 当有15个连接同时请求的时候,前面10个连接请求就被放置在请求队列中,但后面5个请求被拒绝。千万要注意:这个10并不是表示客户端最大的连接数为10, 实际上可以有很多很多的客户端,但是只要不是同时超过设定值进行请求连接就行。在经典书籍《TCP/IP详解 卷1:协议》的Page195有所解释。此参数设定在TCP层接收链接的缓冲池的最大个数。当客户链接请求大于这个个数(缓冲池满),其它的未进入链接缓冲池的客户端在TCP层上TCP模块会自动重新链接,直到超时(大约57秒后)。客户端连接(connect)完成时,服务器端要从TCP层的连接缓冲池中移出一个(accept函数实现),只要accept处理完成一个connect就会将缓冲池空一个,此时可以接着将新的connect连接送入过来处理。
(5)accept函数。
accept函数是TCP协议中服务器端必须使用的函数。用于处理来自服务器的连接。可以说accept函数是所有之中最复杂的函数,但是它的具体功能已经由操作系统帮我们实现了。
主服务器程序(位于服务器端)调用accept函数等待着客户端的connect连接,一旦客户端发起连接,此时就会为这个连接创建一个新的套接字并通过accept函数返回新建socket的描述符.,这个新建的套接字此时已经和发起连接的客户端取得连接。此时我们在开启一个从线程,通过此新建的套接字用于和客户端的通信,专门处理客户端和服务器之间的数据。我们将新建的套接字称之为通信套接字,服务器初始新建并listen之后的套接字专门用于处理连接,我们称之为侦听连接套接字。侦听连接套接字在accept处理一个连接之后,就将新建的通信套接字送入开启的新线程中,在子线程间和客户端进行数据通信,主线程而它专注于处理远端连接请求(使用accept函数)。
总之,在任意时刻服务器中总有一个主线程和零个或多个从服务器线程。主线程用原来初始的套接字接受和处理请求,从线程用新建的套接字和相应的客户端连接并进行双向通信。下图是对UDP和TCP通信的解释。
(6)接受、发送系列函数。recv,recvfrom,send,sendto。
UDP和TCP通信时,这些函数都可以使用到。UDP通信是无连接的,即使没有采用connect时,也可以使用recvfrom和sendto,这两个函数参数较多,recvfrom可以得到对方的源IP地址和端口号,sendto可以设定发送端地址和端口号。但是如果一旦connect则采用send和recv,但是此时采用recvfrom和sendto也适用。只是在TCP通信中,即使采用recvfrom函数,也无法获得对方IP和端口,因为通信之间都是依据处于连接状态的套接字描述符。
recv的recvfrom是可以替换使用的,只是recvfrom多了两个参数,可以用来接收对端的地址信息,这个对于udp这种无连接的,可以很方便地进行回复。而换过来如果你在udp当中也使用recv,那么就不知道该回复给谁了,如果你不需要回复的话,也是可以使用的。另外就是对于tcp是已经知道对端的,就没必要每次接收还多收一个地址,没有意义,要取地址信息,在accept当中取得获得并可以加以记录了。
TCP客户端程序:
// Win32Project1.cpp : 定义应用程序的入口点。
//
#include "stdafx.h"
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <winsock2.h>
#include <stdlib.h>
#define MAX_LOADSTRING 100
#pragma comment(lib,"ws2_32.lib")
int _tmain(int argc, _TCHAR* argv[])
{
int sockfd;
char sendbuff[1000] = {0}, recvbuff[1000] = { 0 };
struct sockaddr_in servaddr, clientaddr,testaddr;
int length = sizeof(sockaddr_in);
WSADATA wdata;
WSAStartup(MAKEWORD(2, 2), &wdata);
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
printf("create socket error\r\n");
exit(0);
}
//客户端也可以绑定套接字
//clientaddr.sin_family = AF_INET;
//clientaddr.sin_port = htons(10032);
//clientaddr.sin_addr.s_addr = htonl(INADDR_ANY);
//if (bind(sockfd, (struct sockaddr*)&clientaddr, sizeof(clientaddr)) == -1){
// printf("bind socket error\r\n");
// exit(0);
//}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(10011);
servaddr.sin_addr.s_addr = inet_addr("192.168.191.1");
//如果对方服务器没有开启则连接会失败
//while (true)
{
if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
printf("connect error,Please make sure Server Open\r\n");
exit(0);
}
while (true)
{
int recebyte = recv(sockfd, (char *)recvbuff, 100, 0);
printf("Receive From Server:%s", recvbuff);
Sleep(2000);
if (strcmp(recvbuff, "Exceeded Max incoming requests, will refused this connect,please Request Later\r\n") == 0)
{
printf("Server is busy now,we will request later\r\n");
closesocket(sockfd);
break;
}
printf("send to server:Communicating\r\n");
send(sockfd, "Communicating", strlen("Communicating") + sizeof(char), 0);
Sleep(1000);
}
}
closesocket(sockfd);
return 0;
}
TCP服务器程序:
// ConsoleApplication1.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <winsock2.h>
#include <stdlib.h>
#define MAX_LOADSTRING 100
#define MAXLINE 100
#define MAXCLIENTS 3 //宏定义,最多3个客户端连接
#pragma comment(lib,"ws2_32.lib")
DWORD WINAPI ProcessClientRequests(LPVOID lpParam);
int _tmain(int argc, _TCHAR* argv[])
{
int listenfd, connfd;
struct sockaddr_in servaddr, clientaddr, testaddr;
char buff[4096];
int recebyte = 0,sendbyte = 0;
HANDLE threads[MAXCLIENTS];
//用于accept函数形参
int length = sizeof(sockaddr_in);
int existingClientCount = 0;
WSADATA wdata;
//在应用程序当中调用任何一个Winsock API函数,首先第一件事情就是必须通过WSAStartup函数完成对Winsock服务的初始化,
//因此需要调用WSAStartup函数。使用Socket的程序在使用Socket之前必须调用WSAStartup函数。
//该函数的第一个参数指明程序请求使用的Socket版本,其中高位字节指明副版本、低位字节指明主版本;
//操作系统利用第二个参数返回请求的Socket的版本信息。当一个应用程序调用WSAStartup函数时,操作系统根据请求的Socket版本来
//搜索相应的Socket库,然后绑定找到的Socket库到该应用程序中。以后应用程序就可以调用所请求的Socket库中的其它Socket函数了。
//WORD MAKEWORD(
// BYTE bLow, //指定新变量的低字节序;
// BYTE bHigh //指定新变量的高字节序;
// );
int ret = WSAStartup(MAKEWORD(2, 2), &wdata);
if (ret != 0){
printf("WSAStartup() failed!\n");
return -1;
}
else{
unsigned char bytehi = HIBYTE(wdata.wVersion);
unsigned char bytelo = LOBYTE(wdata.wVersion);
//确认WinSock DLL支持版本2.2
if (LOBYTE(wdata.wVersion) != 2 || HIBYTE(wdata.wVersion) != 2)
{
WSACleanup(); //释放为该程序分配的资源,终止对winsock动态库的使用
printf("Invalid WinSock version!\n");
return -1;
}
}
//应用程序或DLL在使用Windows Sockets服务之前必须要进行一次成功的WSAStartup()调用.当它完成了Windows Sockets的使用后,
//应用程序或DLL必须调用WSACleanup()将其从Windows Sockets的实现中注销,并且该实现释放为应用程序或DLL分配的任何资源.
//任何打开的并已建立连接的SOCK_STREAM类型套接口在调用WSACleanup()时会重置; 而已经由closesocket()关闭却仍有要发送的悬而
//未决数据的套接口则不会受影响 - 该数据仍要发送.
//新建socket
//AF_INET:IPv4,SOCK_STREAM:TCP协议,SOCK_DGRAM:UDP。最后一个参数0代表根据第二个参数自动选择协议。
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
printf("create socket error\r\n");
exit(0);
}
memset(&servaddr, 0, sizeof(servaddr));
//配置端口和IP地址
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(10011);
//服务器端的绑定,将套接字和端口号绑定
if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
printf("bind socket error\r\n");
exit(0);
}
// listen函数使用主动连接套接字变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。
//在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。
// listen函数一般在调用bind之后 - 调用accept之前调用。 /
//listen函数listenfd代表一个已绑定未被连接的套接字描述符
//第二个参数是等待连接队列的最大长度,比方说,你将backlog定为10, 当有15个连接请求的时候,前面10个连接请求就被放置在请求队列中,
//后面5个请求被拒绝。千千万万要注意:这个10并不是表示客户端最大的连接数为10, 实际上可以有很多很多的客户端(实践证明也是如此)。
//再看函数的返回值,成功返回0, 失败返回 - 1.
//2.《TCP/IP详解 卷1:协议》的Page195详细解释
//1)backlog 用于在TCP层接收链接的缓冲池的最大个数,这个个数可在应用层中的listen函数里设置,
//当客户链接请求大于这个个数(缓冲池满),其它的未进入链接缓冲池的客户端在tcp层上tcp模块会自动重新链接,直到超时(大约57秒后)
//2)应用层链接(connect)完成时,要从tcp层的链接缓冲池中移出一个(accept函数实现),只要accept处理完成一个connect就会将缓冲池空一个
//3.backlog是连接请求队列的最大长度
//1.在WinSock1.1中最大值5。如果backlog小于1,则backlog被置为1;若backlog大于SOMAXCONN(定义在winsock.h中,值为5),
//则backlog被置为SOMAXCONN。
//2.在WinSock2中,没有制定具体值,它由服务提供者决定
//3.有时候backlog设置很小,这时我们接进多少台机器都没问题是因为服务器机器处理速度很快队列来不及填满就处理完了,
//而且在同一个时刻到来的连接还是很少的.
//backlog指的是listen的第二个参数。我们目前是5.意味着可以并发处理5个连接。
if (listen(listenfd, 5) == -1){
printf("listen socket error\r\n");
exit(0);
}
printf("======waiting for client's request======\r\n");
//accept接收连接请求。参数1:服务器端的socket描述符,该套接口在listen()后处于监听连接。
//参数2:addr:(可选)指针,指向一缓冲区,其中为远端客户端的连接的信息。此参数的实际格式由套接口创建时所产生的地址族确定。
//addrlen:(可选)指针,输入参数,配合addr一起使用,指向存有addr地址长度的整型数
//返回客户端socket描述符.
//Listen()并未开始接收连线, 只是设置socket 为listen 模式, 真正处理客户端client中connect连线的是服务器的accept().
while (true){
//clientaddr其中存放着客户端的IP地址和端口信息。accept阻塞等待着客户端的连接
if ((connfd = accept(listenfd, (struct sockaddr*)&clientaddr, &length)) == -1){
printf("accept socket error\r\n");
continue;
}
if (existingClientCount < MAXCLIENTS) //判断是否已经超出最大连接数了
{
threads[existingClientCount] = CreateThread(NULL, 0, ProcessClientRequests, &connfd, 0, NULL);//启动新线程,并且将新建的socket描述符参数传入
CloseHandle(threads[existingClientCount]);
existingClientCount++;
}
else
{
char* msg = "Exceeded Max incoming requests, will refused this connect,please Request Later\r\n";
send(connfd, msg, strlen(msg) + sizeof(char), NULL);//发送拒绝连接消息给客户端。
Sleep(100);
printf("***SYS*** REFUSED.\n");
closesocket(connfd); //释放已经连接的资源
break;//退出循环
}
}
printf("Maximize Clients occurred for d%.\r\n", MAXCLIENTS);
//同时连接请求过多,此时关闭server。
int returnvalue = WaitForMultipleObjects(existingClientCount, threads, TRUE, INFINITE); //等待所有子线程,直到完成为止
closesocket(listenfd);//关闭侦听套接字
//for(int i=0;i<MAXCLIENTS; i++)
//{
// CloseHandle(threads[i]); //清理线程资源
//}
WSACleanup();//清理资源
printf("Cleared all.\r\n");
closesocket(listenfd);
return -1;
}
//处理客户端的子线程
DWORD WINAPI ProcessClientRequests(LPVOID lpParam)
{
SOCKET clientsocket = *(SOCKET*)lpParam; //这里需要强制转换,注意:指针类型
printf("Thread Process Client\r\n");
char buffer[MAXBYTE] = { 0 };
//返回数据给客户端,通知它可以开始会话。
char* msg = "Hello,Client. Let us start this Session\r\n";
send(clientsocket, msg, strlen(msg) + sizeof(char), NULL);
while (TRUE)
{
recv(clientsocket, buffer, MAXBYTE, NULL);
printf("Receive From Client:%s\r\n", buffer);
printf("Send to Client:%s\r\n", buffer);
send(clientsocket, buffer, strlen(buffer) + sizeof(char), 0);
//客户端要求中断处理
if (strcmp(buffer, "exit") == 0)
{
char* msg_bye = "Good Bye\r\n";
send(clientsocket, msg_bye, strlen(msg_bye) + sizeof(char), NULL);
break;
}
}
//此次处理完毕,关闭连接的套接字
closesocket(clientsocket);
return 0;
}
UDP通信较为简单再次不在将代码附上。