1. 程式人生 > >利用OpenSSL實現非阻塞通訊C++程式碼

利用OpenSSL實現非阻塞通訊C++程式碼

可以轉載,轉載請註明出處,謝謝!

這篇博文主要實現瞭如何在win64下基於VS2012實現OpenSSL的非阻塞通訊。參考了以下幾篇博文的內容,表示感謝:

其實關鍵步驟有以下幾步:

1. 讓server和client建立一般的非OpenSSL套接字的時候就設定成非阻塞的,這個應該都清楚怎麼設定。

2. 因為SSL_set_fd中新增的是非阻塞的一般的套接字,所以加上一行

    SSL_set_connect_state(ssl);

    ssl就變成了非阻塞的。

3. SSL_connect、SSL_accept、SSL_read、SSL_write的時候注意,由於是非阻塞的,可能當前緩衝沒有資料,返回是0,此時根據SSL_get_error得到錯誤號,如果是SSL_ERROR_WANT_WRITE或者SSL_ERROR_WANT_READ,應該直接continue,而不應該判錯。這個問題實際上跟一般的winsock套接字程式設計一樣,一般的winsock套接字在設定成非阻塞的時候,如果當前connect、accept、read、write返回是0,不見得是出錯,根據WSAGetLastError可以得到錯誤號可能會是10035,這個錯誤對應的是WSAEWOULDBLOCK,表示緩衝區沒資料,應該阻塞,但由於設定成非阻塞了,所以才報錯。但其實也是不應該判錯的,應該continue。

至於如何配置測試伺服器和客戶端的demo,還是請看我之前的那篇文章的介紹。現在把非阻塞版本的demo伺服器和客戶端程式碼貼下。

伺服器

/***************************************************************** 
*SSL/TLS服務端程式WIN32版(以demos/server.cpp為基礎) 
*需要用到動態連線庫libeay32.dll,ssleay.dll, 
*同時在setting中加入ws2_32.lib libeay32.lib ssleay32.lib, 
*以上庫檔案在編譯openssl後可在out32dll目錄下找到, 
*所需證書檔案請參照文章自行生成. 
*****************************************************************/  

#include <stdio.h>  
#include <stdlib.h>  
#include <memory.h>  
#include <errno.h>  
#include <sys/types.h>  
#include <iostream>
#include <winsock2.h>  

#include "openssl/rsa.h"        
#include "openssl/crypto.h"  
#include "openssl/x509.h"  
#include "openssl/pem.h"  
#include "openssl/ssl.h"  
#include "openssl/err.h"  
using namespace std;
#pragma comment( lib, "ws2_32.lib" )
/*所有需要的引數資訊都在此處以#define的形式提供*/  
#define CERTF   "server.crt" /*服務端的證書(需經CA簽名)*/  
#define KEYF   "server.key"  /*服務端的私鑰(建議加密儲存)*/  
#define CACERT "ca.crt" /*CA 的證書*/  
#define PORT   1111   /*準備繫結的埠*/  

#define CHK_NULL(x) if ((x)==NULL) exit (1)  
#define CHK_ERR(err,s) if ((err)==-1) { perror(s); exit(1); }  
#define CHK_SSL(err) if ((err)==-1) { ERR_print_errors_fp(stderr); exit(2); }  

int main ()  
{  
	int err;  
	int listen_sd;  
	int sd;  
	struct sockaddr_in sa_serv;  
	struct sockaddr_in sa_cli;  
	int client_len;  
	SSL_CTX* ctx;  
	SSL*     ssl;  
	X509*    client_cert;  
	char*    str;  
	char     buf [4096];  
	SSL_METHOD *meth;  
	WSADATA wsaData;  

	if(WSAStartup(MAKEWORD(2,2),&wsaData) != 0){  
		printf("WSAStartup()fail:%d\n",GetLastError());  
		return -1;  
	}  

	OpenSSL_add_ssl_algorithms(); /*初始化*/  
	SSL_load_error_strings();     /*為列印除錯資訊作準備*/  
	SSLeay_add_ssl_algorithms();
	ERR_load_BIO_strings();

	meth = const_cast<SSL_METHOD *>(SSLv3_server_method());
	//meth = const_cast<SSL_METHOD *>(TLSv1_server_method());  /*採用什麼協議(SSLv2/SSLv3/TLSv1)在此指定*/  

	ctx = SSL_CTX_new (meth);  
	CHK_NULL(ctx);  

	//SSL_CTX_set_verify(ctx,SSL_VERIFY_PEER,NULL);   /*驗證與否*/  
	// 伺服器端不需要驗證客戶端的ca,改下就好了,見 https://bbs.csdn.net/topics/390222065
	// 如果選擇SSL_VERIFY_PEER,則SSL_accept處會返回-1,一直報錯
	SSL_CTX_set_verify(ctx,SSL_VERIFY_NONE,NULL);   /*驗證與否*/  

	if(!SSL_CTX_load_verify_locations(ctx,CACERT,NULL)) /*若驗證,則放置CA證書*/  
	{
		ERR_print_errors_fp(stderr);  
		exit(3);  
	}
	if (SSL_CTX_use_certificate_file(ctx, CERTF, SSL_FILETYPE_PEM) <= 0) {  
		ERR_print_errors_fp(stderr);  
		exit(3);  
	}  
	if (SSL_CTX_use_PrivateKey_file(ctx, KEYF, SSL_FILETYPE_PEM) <= 0) {  
		ERR_print_errors_fp(stderr);  
		exit(4);  
	}  

	if (!SSL_CTX_check_private_key(ctx)) {  
		printf("Private key does not match the certificate public key\n");  
		exit(5);  
	}  

	SSL_CTX_set_cipher_list(ctx,"RC4-MD5");   

	/*開始正常的TCP socket過程.................................*/  
	printf("Begin TCP socket...\n");  

	listen_sd = socket (AF_INET, SOCK_STREAM, 0);    
	CHK_ERR(listen_sd, "socket");  

	/********設定套接字非阻塞******/
	ULONG ul = 1;
	int nRet = ioctlsocket(listen_sd,FIONBIO,(unsigned long*)&ul);
	if(nRet == SOCKET_ERROR)
	{
		printf("設定套接字選項--非阻塞失敗!\n");   
		closesocket(listen_sd);
		//WSACleanup();
		return 0;		
	}

	memset (&sa_serv, '\0', sizeof(sa_serv));  
	sa_serv.sin_family      = AF_INET;  
	sa_serv.sin_addr.s_addr = INADDR_ANY;  
	sa_serv.sin_port        = htons (PORT);           

	err = bind(listen_sd, (struct sockaddr*) &sa_serv,  

		sizeof (sa_serv));  

	CHK_ERR(err, "bind");  

	/*接受TCP連結*/  
	err = listen (listen_sd, 5);                     
	CHK_ERR(err, "listen");  

	client_len = sizeof(sa_cli);  
	while (true)
	{
		sd = accept (listen_sd, (struct sockaddr*) &sa_cli, &client_len);  
		if(sd == SOCKET_ERROR)
		{
			err = WSAGetLastError();
			if((err != WSAEWOULDBLOCK))
			{
				cout <<"recvfrom()呼叫失敗,錯誤碼:"<< WSAGetLastError()<<endl;
				break;
			}
			else
			{
				continue;
			}
		}
		else
			break;
	}

	cout << "連線成功!"<< endl;
	closesocket (listen_sd);  

	printf ("Connection from %lx, port %x\n",  
		sa_cli.sin_addr.s_addr, sa_cli.sin_port);  

	/*TCP連線已建立,進行服務端的SSL過程. */  
	printf("Begin server side SSL\n");  

	ssl = SSL_new (ctx);                            
	CHK_NULL(ssl);  
	SSL_set_fd (ssl, sd);  
	SSL_set_accept_state(ssl);

	bool isContinue = true;
	while(isContinue)
	{
		isContinue = false;
		if(SSL_accept(ssl) != 1)
		{
			int icode = -1;
			int iret = SSL_get_error(ssl, icode);
			if ((iret == SSL_ERROR_WANT_WRITE) || (iret == SSL_ERROR_WANT_READ))
			{
				isContinue = true;
			}
			else
			{
				SSL_CTX_free(ctx);
				SSL_free(ssl);
				ctx = NULL;
				ssl = NULL;

				break;
			}
		}
		else
		{
			cout << "SSL連線成功!"<< endl;
			break;
		}
	}


	/*列印所有加密演算法的資訊(可選)*/  
	printf ("SSL connection using %s\n", SSL_get_cipher (ssl));  

	/*這一段不要採用,因為前面設定的是SSL_VERIFY_NONE,伺服器不讀取客戶端的證書。*/  
	client_cert = SSL_get_peer_certificate (ssl);  
	if (client_cert != NULL) {  
		printf ("Client certificate:\n");  

		str = X509_NAME_oneline (X509_get_subject_name (client_cert), 0, 0);  
		CHK_NULL(str);  
		printf ("\t subject: %s\n", str);  
		free (str);  

		str = X509_NAME_oneline (X509_get_issuer_name  (client_cert), 0, 0);  
		CHK_NULL(str);  
		printf ("\t issuer: %s\n", str);  
		free (str);  

		X509_free (client_cert);/*如不再需要,需將證書釋放 */  
	}  
	else  
		printf ("Client does not have certificate.\n");  
	
	
	/* 資料交換開始,用SSL_write,SSL_read代替write,read */  

	int ires = 0, count = 0;;
	bool isCoutinue = true;
	while (true)
	{
		memset(buf,'\0',sizeof(buf));
		count = 0;
		while (isCoutinue)
		{
			ires = SSL_read(ssl, buf + count, 12 - count);
			int nRes = SSL_get_error(ssl, ires);
			if(nRes == SSL_ERROR_NONE)
			{
				if(ires > 0)
				{
					count += ires;
					if (count >= 12)
					{
						cout << buf << endl;
						break;
					}
					continue;
				}
			}
			else if (nRes == SSL_ERROR_WANT_READ)
			{
				continue;
			}
			else
			{
				break;
			}
		}
		Sleep(500);
	}

	/* 收尾工作*/  
	shutdown (sd,2);  
	SSL_free (ssl);  
	SSL_CTX_free (ctx);  

	system("pause");
	return 0;  
}  
/***************************************************************** 
* EOF - serv.cpp 
*****************************************************************/  

客戶端:

/***************************************************************** 
*SSL/TLS客戶端程式WIN32版(以demos/cli.cpp為基礎) 
*需要用到動態連線庫libeay32.dll,ssleay.dll, 
*同時在setting中加入ws2_32.lib libeay32.lib ssleay32.lib, 
*以上庫檔案在編譯openssl後可在out32dll目錄下找到, 
*所需證書檔案請參照文章自行生成*/  
/******************************************************************/  
#include <stdio.h>  
#include <stdlib.h>  
#include <memory.h>  
#include <errno.h>  
#include <sys/types.h>  
#include <iostream>
#include <winsock2.h>  

#include "openssl/rsa.h"        
#include "openssl/crypto.h"  
#include "openssl/x509.h"  
#include "openssl/pem.h"  
#include "openssl/ssl.h"  
#include "openssl/err.h"  
#include "openssl/rand.h"  
using namespace std;


#pragma comment( lib, "ws2_32.lib" )
/*所有需要的引數資訊都在此處以#define的形式提供*/  
#define CERTF  "client.crt"  /*客戶端的證書(需經CA簽名)*/  
#define KEYF  "client.key"   /*客戶端的私鑰(建議加密儲存)*/  
#define CACERT "ca.crt"      /*CA 的證書*/  
#define PORT   1111          /*服務端的埠*/  
#define SERVER_ADDR "127.0.0.1"  /*服務段的IP地址*/  

#define CHK_NULL(x) if ((x)==NULL) exit (-1)  
#define CHK_ERR(err,s) if ((err)==-1) { perror(s); exit(-2); }  
#define CHK_SSL(err) if ((err)==-1) { ERR_print_errors_fp(stderr); exit(-3); }  

int main ()  
{  
	int err;  
	int sd;  
	struct sockaddr_in sa;  
	SSL_CTX* ctx;  
	SSL*     ssl;  
	X509*    server_cert;  
	char*    str = new char[1000];  
	char     buf [4096];  
	SSL_METHOD *meth;  
	int       seed_int[100]; /*存放隨機序列*/  

	WSADATA wsaData;  

	if(WSAStartup(MAKEWORD(2,2),&wsaData) != 0){  
		printf("WSAStartup()fail:%d/n",GetLastError());  
		return -1;  
	}   

	OpenSSL_add_ssl_algorithms(); /*初始化*/  
	SSL_load_error_strings();     /*為列印除錯資訊作準備*/  
	SSLeay_add_ssl_algorithms();
	ERR_load_BIO_strings();

	meth = const_cast<SSL_METHOD*>(SSLv3_client_method()); 
	//meth = const_cast<SSL_METHOD*>(TLSv1_client_method()); /*採用什麼協議(SSLv2/SSLv3/TLSv1)在此指定*/  
	ctx = SSL_CTX_new (meth);                         
	CHK_NULL(ctx);  

	SSL_CTX_set_verify(ctx,SSL_VERIFY_PEER,NULL);   /*驗證與否*/  
	if(!SSL_CTX_load_verify_locations(ctx,CACERT,NULL)) /*若驗證,則放置CA證書*/  
	{
		ERR_print_errors_fp(stderr);  
		exit(3);  
	}

	if (SSL_CTX_use_certificate_file(ctx, CERTF, SSL_FILETYPE_PEM) <= 0) {  
		ERR_print_errors_fp(stderr);  
		exit(-2);  
	}  
	if (SSL_CTX_use_PrivateKey_file(ctx, KEYF, SSL_FILETYPE_PEM) <= 0) {  
		ERR_print_errors_fp(stderr);  
		exit(-3);  
	}  

	if (!SSL_CTX_check_private_key(ctx)) {  
		printf("Private key does not match the certificate public key/n");  
		exit(-4);  
	}   

	/*構建隨機數生成機制,WIN32平臺必需*/  
/*
	srand( (unsigned)time( NULL ) );  
	for( int i = 0;   i < 100;i++ )  
		seed_int[i] = rand();  
	RAND_seed(seed_int, sizeof(seed_int));  */

	/*以下是正常的TCP socket建立過程 .............................. */  
	printf("Begin tcp socket...\n");  

	sd = socket (AF_INET, SOCK_STREAM, 0);       CHK_ERR(sd, "socket");  
	//********設定套接字非阻塞******
	ULONG ul = 1;
	int nRet = ioctlsocket(sd,FIONBIO,(unsigned long*)&ul);
	if(nRet == SOCKET_ERROR)
	{
		printf("設定套接字選項--非阻塞失敗!\n");   
		closesocket(sd);
		//WSACleanup();
		return 0;		
	}

	memset (&sa, '\0', sizeof(sa));  
	sa.sin_family      = AF_INET;  
	sa.sin_addr.s_addr = inet_addr (SERVER_ADDR);   /* Server IP */  
	sa.sin_port        = htons     (PORT);          /* Server Port number */  

	int nSize;

	while (true)
	{
		nSize = connect(sd, (struct sockaddr*) &sa,  sizeof(sa));   
		if(nSize ==0)
		{
			cout << "連線成功!"<< endl;
			break;

		}
		if(nSize < 0)
		{
			err = WSAGetLastError();
			if(err == WSAEWOULDBLOCK)
			{
				Sleep(100);
				continue;
			}
			else if(err == WSAEISCONN)
			{
				cout << "連線成功!"<< endl;
				break;
			}
			else
			{
				cout << "連線失敗!錯誤程式碼:" << WSAGetLastError() << endl;
				closesocket(sd);
				WSACleanup();
				return 0;
			}
		}
	}

	/* TCP 連結已建立.開始 SSL 握手過程.......................... */  
	printf("Begin SSL negotiation \n");  

	ssl = SSL_new (ctx);                          
	CHK_NULL(ssl);  

	SSL_set_fd (ssl, sd);  
	SSL_set_connect_state(ssl);


	bool isContinue = true;
	while(isContinue)
	{
		isContinue = false;
		if(SSL_connect(ssl) == -1)
		{
			int icode = -1;
			int iret = SSL_get_error(ssl, icode);
			if ((iret == SSL_ERROR_WANT_WRITE) || (iret == SSL_ERROR_WANT_READ))
			{
				isContinue = true;
			}
			else
			{
				SSL_CTX_free(ctx);
				SSL_free(ssl);
				ctx = NULL;
				ssl = NULL;

				break;
			}
		}
		else
		{
			cout << "SSL連線成功!"<< endl;
			break;
		}
	}


	/*列印所有加密演算法的資訊(可選)*/  
	printf ("SSL connection using %s\n", SSL_get_cipher (ssl));  

	/*得到服務端的證書並列印些資訊(可選) */  
	server_cert = SSL_get_peer_certificate (ssl);        
	CHK_NULL(server_cert);  
	printf ("Server certificate:\n");  

	str = X509_NAME_oneline (X509_get_subject_name (server_cert),0,0);  
	CHK_NULL(str);  
	printf ("\t subject: %s\n", str);  
	//free (str);  

	str = X509_NAME_oneline (X509_get_issuer_name  (server_cert),0,0);  
	CHK_NULL(str);  
	printf ("\t issuer: %s\n", str);  
	//free (str);  

	X509_free (server_cert);  /*如不再需要,需將證書釋放 */  
	
	/* 資料交換開始,用SSL_write,SSL_read代替write,read */  
	printf("Begin SSL data exchange\n");  

	char* buffer = "Hello World!";
	int ilen = strlen("Hello World!");
	int ires = 0, count = 0;;
	bool isCoutinue = true;
	while (true)
	{
		count = 0;
		while (isCoutinue)
		{
			ires = SSL_write(ssl, buffer + count, ilen - count);
			int nRes = SSL_get_error(ssl, ires);
			if(nRes == SSL_ERROR_NONE)
			{
				if(ires > 0)
				{
					count += ires;
					if (count >= ilen)
					{
						printf("Write finished!\n");  

						break;
					}
					continue;
				}
			}
			else if (nRes == SSL_ERROR_WANT_READ)
			{
				continue;
			}
			else
			{
				break;
			}
		}
		Sleep(500);
	}


	SSL_shutdown (ssl);  /* send SSL/TLS close_notify */  

	/* 收尾工作 */  
	shutdown (sd,2);  
	SSL_free (ssl);  
	SSL_CTX_free (ctx);  
	system("pause");

	return 0;  
}  
/***************************************************************** 
* EOF - cli.cpp 
*****************************************************************/  

同樣的,在執行的剛開始,客戶端和伺服器的伺服器私鑰密碼都是輸入123456(這個密碼是在建立金鑰的時候自己設定的,可以參考我的其他博文,也可以參考網上其他人的博文),然後回車,結果如下: