利用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(這個密碼是在建立金鑰的時候自己設定的,可以參考我的其他博文,也可以參考網上其他人的博文),然後回車,結果如下: