在Windows环境下,套接口的通信方式分为两种:阻塞方式和非阻塞方式。阻塞方式下工作的套接口在进行I/O操作时,函数要等待到相关操作完成
后才能返回(或者可以使用WSACancelBlockingCall( )调用唤起一个阻塞操作)。
本篇博客实现windows下的阻塞IO socket编程。
简单TCP阻塞模式:
const char HOST_ADDRESS[] = "127.0.0.1";
const int HOST_PORT = 8888;
const int DEFAULT_BUFFER = 1<<12; //4k
void testClient()
{
WSADATA wsaData;
//typedef unsigned short WORD;16位无符号整形
WORD version = MAKEWORD(2,2);//0x0202
int err = WSAStartup(version,&wsaData);
//cout<<(int)HIBYTE(version)<<(LOBYTE(version)==2);
if(0 != err){
cerr<<"Socket2.2 初始化失败!,Exit";
exit(0);
}
SOCKET sockClient = socket(AF_INET,
SOCK_STREAM,
0);
if(INVALID_SOCKET == sockClient){
cerr<<"Socket创建失败!Exit";
WSACleanup();
exit(0);
}
//设置发送地址
sockaddr_in addrto;
memset(&addrto,0,sizeof(addrto));
addrto.sin_family = AF_INET;//tcp/ip系的协议
addrto.sin_addr.s_addr = inet_addr(HOST_ADDRESS);
addrto.sin_port = htons(HOST_PORT);
if(connect(sockClient,(SOCKADDR*) &addrto,sizeof(addrto)) ==SOCKET_ERROR){
cerr<<"Failed to connect.\n";
WSACleanup();
exit(0);
}
//发送和接受数据
int bytesSend,left,idx = 0;
int bytesRecv = SOCKET_ERROR;
char sendbuf[DEFAULT_BUFFER] = "";
char recvbuf[DEFAULT_BUFFER] = "";
while(true){
cout<<"[发送数据到服务器]:";
cin>>sendbuf;
//cout<<sendbuf;
left = strlen(sendbuf);
//分多次发送数据
while(left>0){
bytesSend = send(sockClient,&sendbuf[idx],left,0);
if(bytesSend == 0)
exit(-1);
if(bytesSend == SOCKET_ERROR){
cout<<"Send Failed:"<<WSAGetLastError()<<endl;
exit(-1);
}
left -= bytesSend;
idx += bytesSend;
}
idx = 0;
while(bytesRecv == SOCKET_ERROR){
bytesRecv = recv(sockClient,recvbuf,DEFAULT_BUFFER,0);
if(bytesRecv == 0 || bytesRecv == WSAECONNRESET){//远程停止了虚电路
cout<<"连接已关闭!"<<endl;
WSACleanup();
closesocket(sockClient);
exit(-1);
}
if(bytesRecv<0) {
cout<<"未收到回复"<<endl;
exit(-1);
}
cout<<"[从服务器接收的数据]:"<<recvbuf<<endl;
}
bytesRecv = SOCKET_ERROR;
memset(sendbuf,0,DEFAULT_BUFFER);
memset(recvbuf,0,DEFAULT_BUFFER);
}
}
void StartTCPServer()
{
WSADATA wsa;
WORD version = MAKEWORD(2,2);
int ret;
ret = WSAStartup(version,&wsa);
if(ret != NO_ERROR){
cout<<"初始化socket失败!"<<endl;
return;
}
SOCKET listenSocket;//监听的socket
SOCKET clientSocket;//客户端d
const char* adr = "127.0.0.1";
const unsigned port = 8088;
listenSocket = socket(AF_INET,SOCK_STREAM,0);
SOCKADDR_IN serverAdrr;
serverAdrr.sin_family = AF_INET;
serverAdrr.sin_port = htons(port);
//serverAdrr.sin_addr.S_un.S_addr = inet_addr(adr);
serverAdrr.sin_addr.s_addr = INADDR_ANY;
//绑定服务器的socket与服务器地址
ret = bind(listenSocket,(LPSOCKADDR)&serverAdrr,sizeof(serverAdrr));
if(ret == SOCKET_ERROR){
cout<<"绑定socket失败:"<<WSAGetLastError()<<endl;
return;
}
ret = listen(listenSocket,2);
if(ret == SOCKET_ERROR){
cout<<"监听端口失败:"<<WSAGetLastError()<<endl;
return;
}
clientSocket = accept(listenSocket,NULL,NULL);
if(ret == SOCKET_ERROR){
cout<<"监听端口失败:"<<WSAGetLastError()<<endl;
return;
}
const unsigned size = 1<<8;
char buffer[size];
//buffer = new char;
memset(buffer,0,sizeof(buffer));
const char * str = "连接服务器成功...";
strcpy_s(&buffer[0],sizeof(buffer),str);
ret = send(clientSocket,buffer,sizeof(buffer),0);
cout<<"已发送的字节数:"<<ret<<endl;
do{
memset(buffer,0,sizeof(buffer));
ret = recv(clientSocket,buffer,size,0);
if(ret > 1){
cout<<ret<<endl;
cout<<"客户端回复:"<<buffer<<endl;
}
}while(true);
}
上面例子实现了windows下阻塞模式TCP流式套接字编程。基本流程如下:
服务器端的具体步骤:
WSADATA lpWSAData;
WSAStartup(MAKEWORD(2,2),&lpWSAData);
bind(sktConnect,(struct sockaddr far *)&sockaddrin,sizeof(sockaddrin));
4.服务端监听网络:
listen(sktConnect,1);
sktClient=accept(sktConnect,(struct sockaddr far *)&sockaddrin,& sockaddrlen);
send(sktClient,chrSend,10,0);recv(sktClient,chrReceive,10,0);
客户端的具体步骤:
1.初始化与创建客户端Winsock:
首先WSAStartup函数来填充WSADATA结构,随后同样调用socket函数创建客户端的套接字,给客户端Sockaddr_in结构赋值,地址类型和端口号与服务端相同
2.套接字选项设置:
使用setsockopt函数设置套接字选项,比如发送或者接收的超时设置,缓冲区的容量设置,使用ioctlsocket函数设置socket的I/O模式等;
int ret=ioctlsocket(sktClient,FIONBIO,(unsigned long*)&ul);
3.双方建立连接:
connect(sktClient,(const struct sockaddr *)&sockaddrin,sizeof (sockaddrin));
客户端调用connect函数向服务端发出连接请求,当连接请求到来时,被阻塞服务端的accept函数生成一个新的字节流套接字,返回客户端Sockaddr_in结构变量,用带有客户端地址的套接字与客户端进行连接,然后向客户端返回接收信号;
4.收发数据
5.关闭套接字与winsock注销:
服务端和客户端可以通过调用closesocket函数关闭套接字上的所有发送和接收操作,撤销套接字并且中断连接。同时,winsock服务的动态链接库在使用结束后,应用程序必须调用WSACleanup函数将其注销,并释放分配的资源。
下面来看一些基本的概念:
1.套接字(socket)
它是网络的基本构件。它是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连听进程。套接字存在通信区域(通信区域又称地址簇)中。套接字只与同一区域中的套接字交换数据(跨区域时,需要执行某和转换进程才能实现)。WINDOWS 中的套接字只支持单一通信域。
套接字是一个int类型整数,类似于文件描述符或者HANDLE。
套接字分为流套接字(SOCK_STREAM)和数据报套接字(SOCK_DGRAM)。流套接字提供双向有序无重复且无记录边界的数据流服务,它适用于处理大量数据。是面向连接的,通信双方进行数据交换之前,必须建立一条路径。若使用TCP协议则应把套接字设置为流式的。
数据报套接字支持双向的数据流,但并不保证传输的可靠性有序性和无重复性,是无连接的。适用于UDP协议。
2.套接字地址(sockaddr与sockaddr_IN)
struct sockaddr{
u_short sa_family;
char sa_data[14];
};
sa_family是socket address family的缩写,表示地址种类,一般用AF_INET表示该socket处于internet域。另外的14字节是用来描述地址的。这是一种通用结构,事实上,当我们指定sa_family=AF_INET之后,sa_data的形式也就被固定了下来:最前端的2字节用于记录16位的端口,紧接着的4字节用于记录32位的IP地址,最后的8字节清空为零。
该地址结构随所选择的协议的不同而变化,因此一般情况下另一个与该地址结构大小相同的sockaddr_in结构更为常用,sockaddr_in结构用来标识TCP/IP协议下的地址,可强制将sockaddr_in转换为sockaddr。sockaddr_in的结构格式如下:
struct sockaddr_in{
short sin_family;
unsigned short sin_port;//端口
struct in_addr sin_addr;
char sin_zero[8];
};
struct in_addr
{
unsigned long s_addr;
};
s_addr是ip地址,可调用inet_addr函数把类似“127.0.0.1”的字符串转换成long。
3.Winsock的启动和终止
typedef struct WSAData{
WORD wVersion; //Winsock版本
WORD wHightVersion;
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];
unsigned short iMaxSocket; //打开的套接字的最大长度
unsigned short iMaxUdpDg; //打开的数据报的最大长度
char FAR* lpVendorInfo;
}WSADATA,*LPWSADATA;
下面来实现udp协议收发消息:
#pragma once
#include <WinSock2.h>
#include <tchar.h>
#include <process.h>
#include <string>
#include <iostream>
#pragma comment(lib,"Ws2_32.lib")
using namespace std;
//-----------------------------------------------
//udp客户端
//-----------------------------------------------
const char* DEFAULT_HOST = "127.0.0.1";
const int DEFAULT_PORT = 8088;
struct sockfd
{
int fd;
SOCKADDR* addr;
sockfd():fd(),addr(NULL){}
~sockfd(){addr = NULL;}
};
int InitSocket()
{
WSADATA wsa;
if (WSAStartup(0x202,&wsa)!= NO_ERROR) {
cerr<<"初始化Socket错误"<<endl;
return -1;
}
return 0;
}
//-----------------------------------------------
//循环发送所有的buffer
//-----------------------------------------------
int sendAll(sockfd* sc,const char* buffer,size_t sz)
{
int err = 0;
int sen = 0;
while (true) {
err = sendto(sc->fd,buffer,sz - sen,0,sc->addr,sizeof SOCKADDR_IN);
if (err <= 0) {
return err;
}
sen += err;
if (sen == sz) {
cout<<"发送消息:"<<buffer<<" 共"<<sz<<"字节"<<endl;
break;
}
}
return sen;
}
int readAll(sockfd* sc,char* buffer,size_t sz)
{
int err = 0;
int len = sizeof SOCKADDR_IN;
while (true) {
err = recvfrom(sc->fd,buffer,sz,0,sc->addr,&len);
if (err <= 0) {
//cerr<<""<<endl;
return err;
}
break;
}
return err;
}
客户端类:
#pragma once
#include "../Common/UDP.h"
//发送消息的线程
unsigned WINAPI SendTh(LPVOID lparam)
{
sockfd* sc = reinterpret_cast<sockfd*>(lparam);
unsigned char head[2];
char buffer[1024];
while (!cin.bad()){
memset(buffer,0,sizeof buffer);
cin>>buffer;
//先发送消息头部
size_t len = strlen(buffer);//消息的实际长度
unsigned char head[2];//2Byte最大可以表示(1<<16)-1
head[0] = (len>>8) & 0xff; //低位
head[1] = len & 0xff; //高位
int r = sendAll(sc,(char*)head,2);
if (r < 0) {
cerr<<"发送头部失败"<<endl;
return -1;
}
//再发送消息主体
r = sendAll(sc,buffer,len);
if (r < 0) {
cerr<<"发送消息失败"<<endl;
return -1;
}
}
return 0;
}
//接受消息的线程
unsigned WINAPI RecvTh(LPVOID lparam)
{
sockfd* sc = reinterpret_cast<sockfd*>(lparam);
while (true) {
unsigned char head[2];
memset(head,0,2);
if (readAll(sc,(char*)head,2) == 0) {
break;
}
size_t len = (head[0]<<8) | head[1];
if (len > 0) {
//接收消息
char* temp = new char[len+1];
memset(temp,0,len+1);
readAll(sc,temp,len);
temp[len] = '\0';
cout<<"从服务器端收到消息:"<<temp<<" 共:"<<len<<"字节"<<endl;
delete temp;
}
}
return 0;
}
int _tmain(int args,TCHAR* argv[])
{
InitSocket();
SOCKET clientSock = socket(AF_INET,SOCK_DGRAM,0);
SOCKADDR_IN addr;
memset(&addr,0,sizeof addr);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(DEFAULT_HOST);
addr.sin_port = htons(DEFAULT_PORT);
sockfd* sc = new sockfd;
sc->fd = clientSock;
sc->addr = reinterpret_cast<LPSOCKADDR>(&addr);
cout<<"------------------UDP客户端已启动-----------------"<<endl;
//开启线程
unsigned thid;
HANDLE send = (HANDLE)_beginthreadex(NULL,0,SendTh,sc,0,&thid);
HANDLE recive = (HANDLE)_beginthreadex(NULL,0,RecvTh,sc,0,&thid);
WaitForSingleObject(send,INFINITE);
WaitForSingleObject(recive,INFINITE);
CloseHandle(send);
CloseHandle(recive);
delete sc;
closesocket(clientSock);
WSACleanup();
system("pause");
return 0;
}
服务器类:接收客户端消息并返回给客户端
#pragma once
#include "../Common/UDP.h"
void SendMsg(sockfd* sc,const char* buffer)
{
unsigned char head[2];
memset(head,0,2);
size_t len = strlen(buffer);
head[0] = (len >> 8) & 0xff;
head[1] = len & 0xff;
int r = sendAll(sc,(char*)head,2);
if (r < 0) {
cerr<<"发送头部失败"<<endl;
}
r = sendAll(sc,buffer,len);
if (r < 0) {
cerr<<"发送消息失败"<<endl;
}
}
void RecvMsg(sockfd* sc)
{
unsigned char head[2];
while (true){
//接收消息头部
memset(head,0,2);
if (readAll(sc,(char*)head,2) == 0) {
break;
}
size_t len = head[0]<<8 | head[1];
char* msg = new char[len+1];
readAll(sc,msg,len);
msg[len] = '\0';
cout<<"从客户端收到消息:"<<msg<<" 共"<<len<<"字节"<<endl;
//把消息返回给客户端
SendMsg(sc,msg);
delete msg;
}
}
int _tmain(int argc,TCHAR* argv[])
{
InitSocket();
SOCKET reciveSocket = socket(AF_INET,SOCK_DGRAM,0);
SOCKADDR_IN serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr(DEFAULT_HOST);
serverAddr.sin_port = htons(DEFAULT_PORT);
int iResult = bind(reciveSocket,reinterpret_cast<LPSOCKADDR>(&serverAddr),
sizeof serverAddr);
if (iResult == SOCKET_ERROR) {
cerr<<"绑定端口失败"<<endl;
}
sockfd* sc = new sockfd;
sc->fd = reciveSocket;
sc->addr = reinterpret_cast<LPSOCKADDR>(&serverAddr);
cout<<"------------------UDP服务器端已启动-----------------"<<endl;
//开始接受消息
RecvMsg(sc);
delete sc;
closesocket(reciveSocket);
WSACleanup();
system("pause");
return 0;
}