Socket编程基础(一)

在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流式套接字编程。基本流程如下:


服务器端的具体步骤:

1.初始化winsock库
在调用Socket前先要初始化,即加载相应版本的DLL,通过调用WSAStartup函数,将加载成功的Socket库版本的相关信息填在LPWSADATA结构中;
WSADATA lpWSAData;
    WSAStartup(MAKEWORD(2,2),&lpWSAData);
2.创建Socket:
完成初始化之后,调用socket函数创建一个套接字,返回套接字句柄,在其后通信中始终用来标识套接字,若调用失败则返回INVALID_SOCKET;
3.绑定Socket
绑定服务端地址:在为某种特定协议创建了套接字后,就用bind函数将套接字绑定到一个本机地址,其类型是sockaddr,用于指明套接字绑定地址,包括IP地址与端口号;
bind(sktConnect,(struct sockaddr far *)&sockaddrin,sizeof(sockaddrin));
4.服务端监听网络:
socket利用listen函数设置状态位,用来检测是否有到来的连接请求,然后调用accept函数,准备接收客户端连接信号,无连接请求时,服务进程被阻塞;
listen(sktConnect,1);
    sktClient=accept(sktConnect,(struct sockaddr far *)&sockaddrin,& sockaddrlen);
5.收发数据:
一旦客户端套接字接收到来自服务端的接受信号,则表示双方已经实现连接,任何一方均可使用Send/Write函数和Recv/Read函数向对方发送或者接收数据;
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结构如下:
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];
};
in_addr显然是internet address,其结构如下:
struct in_addr
{
    unsigned long s_addr;
};
s_addr是ip地址,可调用inet_addr函数把类似“127.0.0.1”的字符串转换成long。

3.Winsock的启动和终止

Winsock的服务是以动态链接库Winsock DLL形式实现的,所以必须先调用WSAStartup函数对Winsock DLL进行初始化,设定版本号,并分配必
要资源。WSAStartup函数原型如下:
int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);
其中wVersionRequested用于指定Winsock库的版本,通常高位字节指定副版本,低位字节则是主版本。然后用宏MAKEWORD(X,Y)(X为高位字节,Y是低位字节)获取wVersionRequested的正确值。lpWSAData参数是指向LPSWSADATA结构的指针,该结构包含了加载库版本有关的信息,它的格式如下:
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协议收发消息:

udp.h
#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;
}


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值