Socket
当通信过程不相关且不共享文件系统时,可以采用何种机制?这就需要一个通用的传输层通信子系统驻留在每个参与通信的主机上,如TCP/IP。TCP/IP是Internet上使用的通用消息传输协议族。该子系统将网络上的一个端点的消息路由并传递到另一个端点。Unix套接字接口是一种将程序的地址空间(a socket)中的引用对象与底层消息传输子系统中的通信端点(a port)绑定的方式,以实现在网络软件连接的端点之间进行消息传递。
在网络通信中,服务器首先创建一个本地套接字,并将其绑定到通信子系统中唯一的端口地址。然后,它告诉通信,使这成为一个侦听端口,以便它可以创建一个相关的请求队列。然后,服务器等待进入的连接。客户端创建一个本地套接字,并要求通信子系统使用服务器的已知端口号将其套接字连接到远程服务器。每个被接受的连接建立一个新的临时服务器端套接字,用于处理特定的客户机/服务器交换。例如,允许多个线程使用相同的服务器端口号处理与客户端的独立连接。
连接被接受和建立后,通信通过写入客户端的本地套接字进行,数据通过网络被TCP/IP透明地携带,并可通过另一端的服务器套接字接收。服务器解析并作用于请求,然后可以将响应写入自己的套接字并关闭连接。然后将响应返回给客户端在TCP/IP协议下,客户端将从其套接字中读取响应,然后关闭其侧的连接。
实现消息交换的Socket接口的基本调用序列。
TCP
TCP 服务端步骤
记得配置好gcc,gcc使用可参考:gcc与Makefile
/*************** Server Code ****************/
#include <stdio.h> /* for printf() and fprintf() */
#include <sys/socket.h> /* for socket(), bind(), and connect() */
#include <arpa/inet.h> /* for sockaddr_in and inet_ntoa() */
#include <stdlib.h> /* for atoi() and exit() */
#include <string.h> /* for memset() */
#include <unistd.h> /* for close() */
#define MAXPENDING 5 /* Maximum outstanding connection requests */
#define RCVBUFSIZE 32 /* Size of receive buffer */
int main(int argc, char *argv[])
{
int servSock; /* Socket descriptor for server */
int clntSock; /* Socket descriptor for client */
struct sockaddr_in echoServAddr; /* Local address */
struct sockaddr_in echoClntAddr; /* Client address */
unsigned short echoServPort; /* Server port */
unsigned int clntLen; /* Length of client address data structure */
char echoBuffer[RCVBUFSIZE]; /* Buffer for receiving client's msg string */
int recvMsgSize; /* Size of received message */
char *echoString; /* Server's reply to client */
unsigned int echoStringLen; /* Length of server's reply string */
echoString = "server is alive, how are you?"; /* Server's reply to client */
echoStringLen = 29;
echoServPort = 23; /* local port on which server is going to listen */
/* Create local TCP/IP socket for incoming connections */
servSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
/* Construct local address structure */
memset(&echoServAddr, 0, sizeof(echoServAddr)); /* Zero out structure */
echoServAddr.sin_family = AF_INET; /* Internet address family */
echoServAddr.sin_addr.s_addr = htonl(INADDR_ANY); /* Any incoming interface */
echoServAddr.sin_port = htons(echoServPort); /* Local port */
/* Bind local socket to the desired server port address */
int flag = bind(servSock, (struct sockaddr *) &echoServAddr, sizeof(echoServAddr));
if (flag == -1) {
printf("error");
return 0;
}
/* Mark the socket so it will listen for incoming connections */
listen(servSock, MAXPENDING);
for (;;) /* Run forever */
{
/* Set the size of the in-out parameter */
clntLen = sizeof(echoClntAddr);
/* Blocking wait for a client to connect */
clntSock = accept(servSock, (struct sockaddr *) &echoClntAddr, &clntLen);
/* clntSock is connected to a client! */
printf("Server: Handling client %s\n", inet_ntoa(echoClntAddr.sin_addr));
/* Receive message from client */
recvMsgSize = recv(clntSock, echoBuffer, RCVBUFSIZE, 0);
printf("Server: Received msg-> %s\n", echoBuffer);
/* Send response message back to client */
send(clntSock, echoString, echoStringLen, 0);
printf("Server: Sent Reply-> %s\n", echoString);
close(clntSock); /* Close client socket */
}
return 0;
}
TCP 客户端步骤
/**************** Client Code ***************/
#include <stdio.h> /* for printf() and fprintf() */
#include <sys/socket.h> /* for socket(), connect(), send(), and recv() */
#include <arpa/inet.h> /* for sockaddr_in and inet_addr() */
#include <stdlib.h> /* for atoi() and exit() */
#include <string.h> /* for memset() */
#include <unistd.h> /* for close() */
#define RCVBUFSIZE 32 /* Size of receive buffer */
int main(int argc, char *argv[])
{
int sock; /* Socket descriptor */
struct sockaddr_in echoServAddr; /* Echo server address */
unsigned short echoServPort = 23; /* Echo server port */
char *servIP; /* Server IP address (dotted quad) */
char *echoString; /* String to send to echo server */
char echoBuffer[RCVBUFSIZE]; /* Buffer for echo string */
unsigned int echoStringLen; /* Length of string to echo */
int bytesRcvd, totalBytesRcvd; /* Bytes read in single recv() and total bytes read */
servIP = "10.0.65.0"; /* server IP address (this host's own address) */
echoString = "hello"; /* string to echo */
echoStringLen = 5; /* Length of string to echo */
/* Create a local client stream socket using TCP */
sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
/* Construct the server address structure */
memset(&echoServAddr, 0, sizeof(echoServAddr)); /* Zero out structure */
echoServAddr.sin_family = AF_INET; /* Internet address family */
echoServAddr.sin_addr.s_addr = inet_addr(servIP); /* Server IP address */
echoServAddr.sin_port = htons(echoServPort); /* Server port */
/* Establish the connection to the server */
connect(sock, (struct sockaddr *) &echoServAddr, sizeof(echoServAddr));
/* send the message over the socket connection */
send(sock, echoString, echoStringLen, 0);
printf("Client: Sent greeting-> %s\n", echoString);
/* Receive a reply back from the server */
/* Receive up to the buffer size (minus 1 to leave space for a null terminator) bytes from the sender */
bytesRcvd = recv(sock, echoBuffer, RCVBUFSIZE - 1, 0);
echoBuffer[bytesRcvd] = '\0'; /* Terminate the string! */
printf("Client: Received Reply-> %s\n", echoBuffer); /* Print the string received from server */
close(sock);
exit(0);
}
之后如果有需要会发出Windows下Java版本的TCP/IP,并且Java版本也有TLS协议的文章参考:SSL/TLS协议详解以及配置实战
最终实现
TLS
TCP SSL 服务端步骤
- 先附上Windows下的参考文章:基于openssl实现TCP双向认证
- openssl的版本,本文用的1.0.2p,这个版本支持 TLSv1.2,但不支持 TLSv1.3。所以在使用时应该选择相应的加密套件和协议版本,以确保与其他设备和服务的兼容性。
- 要先生成CA的私钥和证书:SSL/CA 证书及其相关证书文件(pem、crt、cer、key、csr)
生成CA的私钥
openssl genrsa -out server.key 2048
生成CA的请求文件
openssl req -new -key server.key -out server.csr
生成证书
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
上面三步完成后,在文件夹下会得到三个文件:
server.key
server.csr
server.crt
- 查看生成的密钥和证书信息,参考:ubuntu用openssl生成私钥和证书步骤
- 记得注意密钥和证书的位置和权限,路径要放在同个文件夹,一开始没配置好花了很长时间。
gcc编译
gcc -o server server.c -lssl -lcrypto
#include <stdio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#define MAXPENDING 5
#define RCVBUFSIZE 1024
int main() {
int servSock, clntSock;
struct sockaddr_in echoServAddr, echoClntAddr;
unsigned short echoServPort = 23;
char echoBuffer[RCVBUFSIZE] = "";
char echoString[1024] = "server is alive, how are you?";
unsigned int echoStringLen = strlen(echoString);
SSL_CTX *ctx;
SSL *ssl;
SSL_library_init();
//1.0.2p这个版本支持 TLSv1.2,但不支持 TLSv1.3。所以在使用时应该选择相应的加密套件和协议版本,以确保与其他设备和服务的兼容性。
ctx = SSL_CTX_new(TLSv1_2_method());
// Load certificate
SSL_CTX_use_certificate_file(ctx, "server.crt", SSL_FILETYPE_PEM);
// Load private key
SSL_CTX_use_PrivateKey_file(ctx, "server.key", SSL_FILETYPE_PEM);
if (!SSL_CTX_check_private_key(ctx)) {
printf("SSL_CTX_check_private_key failed\n");
return 0;
}
SSL_CTX_set_timeout(ctx, 100);
servSock = socket(AF_INET, SOCK_STREAM, 0);
if (servSock == -1) {
printf("socket create failed\n");
return 0;
}
memset(&echoServAddr, 0, sizeof(echoServAddr));
echoServAddr.sin_family = AF_INET;
echoServAddr.sin_addr.s_addr = htonl(INADDR_ANY);
echoServAddr.sin_port = htons(echoServPort);
if (bind(servSock, (struct sockaddr *)&echoServAddr, sizeof(echoServAddr)) == -1) {
printf("socket bind failed\n");
return 0;
}
if (listen(servSock, MAXPENDING) == -1) {
printf("socket listen failed\n");
return 0;
}
printf("tcp ssl server:%d\n", echoServPort);
for (;;) {
unsigned int clntLen = sizeof(echoClntAddr);
clntSock = accept(servSock, (struct sockaddr *)&echoClntAddr, &clntLen);
printf("Server: Handling client %s\n", inet_ntoa(echoClntAddr.sin_addr));
SSL* ssl = SSL_new(ctx);
if (ssl == NULL) {
printf("socket:%d,SSL_new failed\n", clntSock);
goto freessl;
}
SSL_set_fd(ssl, clntSock);
if (SSL_accept(ssl) < 0) {
printf("socket:%d,SSL_accept failed\n", clntSock);
goto freessl;
}
SSL_read(ssl, echoBuffer, sizeof(echoBuffer));
printf("Server: Received msg-> %s\n", echoBuffer);
SSL_write(ssl, echoString, echoStringLen);
printf("Server: Sent Reply-> %s\n", echoString);
freessl:
SSL_shutdown(ssl);
close(clntSock);
// Free SSL
if (ssl)
SSL_free(ssl);
continue;
}
return 0;
}
TCP SSL 客户端步骤
这里写了很多debug的代码,本文编辑之初衷就是自己花了一天半在解决bug。
#include <stdio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#define RCVBUFSIZE 1024
int main() {
int sock;
struct sockaddr_in server;
char message[1024] = "hello";
char buffer[RCVBUFSIZE] = "";
SSL_CTX *ctx;
SSL *ssl;
int res = 0;
SSL_library_init();
ctx = SSL_CTX_new(TLSv1_2_method());
sock = socket(AF_INET, SOCK_STREAM, 0);
server.sin_addr.s_addr = inet_addr("10.0.65.0");
server.sin_family = AF_INET;
server.sin_port = htons(23);
if (connect(sock, (struct sockaddr *)&server, sizeof(server)) == -1) {
printf("socket connect failed\n");
}
printf("connect client, socket:%d\n", sock);
ssl = SSL_new(ctx);
if (ssl == NULL) {
printf("socket:%d, SSL_new failed\n", sock);
goto freessl;
}
SSL_set_fd(ssl, sock);
if (SSL_connect(ssl) < 0) {
printf("socket:%d, SSL_accept failed\n", sock);
goto freessl;
}
printf("ssl connected ok\n");
int result = SSL_write(ssl, message, strlen(message));
if (result <= 0) {
int error = SSL_get_error(ssl, result);
switch (error) {
case SSL_ERROR_ZERO_RETURN:
printf("SSL_write error: Connection closed\n");
break;
case SSL_ERROR_WANT_READ:
case SSL_ERROR_WANT_WRITE:
printf("SSL_write error: Try again later\n");
break;
case SSL_ERROR_SYSCALL:
perror("SSL_write error");
break;
default:
printf("SSL_write error: %d\n", error);
break;
}
}
SSL_read(ssl, buffer, sizeof(buffer));
printf("Client: Received Reply-> %s\n", buffer);
freessl:
SSL_shutdown(ssl);
close(sock);
// Free SSL
if (ssl)
SSL_free(ssl);
return 0;
}
最终实现
我这边是通过交叉编译在通过putty访问远程服务器进行实现,具体代码的ip和端口需要自行配置。本地ip:127.0.0.1,port:8080。
tips1:端口不要和putty的冲突了。我putty是22,这里就配置的23。
tips2:在深入学习后发现,TLS协议最好使用443端口。