在网络编程编程中,我们经常会遇到这样一种C/S架构,服务器端(Server)监听客户端(Client)发送过来的命令,然后解析该命令,并做对应的处理,
最后返回处理结果(例如成功或者失败及原因)给客户端。
最近,在Linux下做网络编程,涉及的就是上面的这种需求,简单地整理了下自己的代码,分享在这里吧,供初学者参考。
首先说一下编程思路吧。
在这种情况客户端必须实现的的接口有:连接服务器、发送、断开连接。
服务器端,有一个主线程,用于监听客户端的连接请求,一旦有新客户端连接,则创建一个新的socket及线程专门服务这个客户端。这个服务线程专门监听该客户端的命令,并且解析命令进行服务器,直到客户端断开连接或者发送关闭连接的命令。
另外,需要涉及一个通信协议,约定命令的包头、命令的识别码、命令的参数。
思路就说到这儿了,下面的相关代码,附件中有完整的代码,包含了Makefile文件。
一、通信协议设计
//
// COPYRIGHT NOTICE
// Copyright (c) 2011, 华中科技大学 ticktick(版权声明)
// All rights reserved.
//
/// @file Command.h
/// @brief 命令包声明文件
///
/// 定义各种TCP命令包以及相关结构体
///
/// @version 1.0
/// @author lujun
/// @E-mail lujun.hust@gmail.com
/// @date 2011/08/21
//
//
// 修订说明:
//
#ifndef COMMAND_H_
#define COMMAND_H_
typedef unsigned char uint8_t;
// TCP CMD header len
#define TCP_CMD_HEADER_LEN 8
// CMD header
static uint8_t TCP_CMD_HEADER_STR[TCP_CMD_HEADER_LEN] = { 0xAA,0xA1,0xA2,0xA3,0xA4,0xA5,0xA6,0xFF };
// max user name len
#define MAX_USER_NAME_LEN 20
// server cmd struct
typedef enum _ServerCMD
{
CMD_SAVE_USER_NAME, // save user name
CMD_SAVE_USER_AGE, // save user age
}ServerCMD;
// return cmd
typedef enum _ReturnCMD
{
DVS_RETURN_SUCCESS = 0,
DVS_RETURN_FAIL,
DVS_RETURN_TIMEOUT,
DVS_RETURN_INVLID_HEADER,
DVS_RETURN_INVLID_CMD,
DVS_RETURN_INVLID_PRM,
}ReturnCMD;
// 1bytes aligning
#pragma pack( push, 1 )
// server pack from client
typedef struct _ServerPack
{
// cmd header
uint8_t cmdHeader[TCP_CMD_HEADER_LEN];
// command id
ServerCMD serverCMD;
// cmd param
union
{
// save user name
struct
{
// user name
char username[MAX_USER_NAME_LEN];
}UserName;
// save user age
struct
{
// user age
int userage;
}UserAge;
}Parameters;
}ServerPack;
// return pack from server
typedef struct _ReturnPack
{
// cmd header
uint8_t cmdHeader[TCP_CMD_HEADER_LEN];
// return cmd
ReturnCMD returnCMD;
}ReturnPack;
#pragma pack( pop )
#define SERVER_PACK_LEN sizeof(ServerPack)
#define RETURN_PACK_LEN sizeof(ReturnPack)
#endif // COMMAND_H_
二、客户端代码
//
// COPYRIGHT NOTICE
// Copyright (c) 2011, 华中科技大学 ticktick(版权声明)
// All rights reserved.
//
/// @file client.c
/// @brief tcp客户端代码
///
/// 实现tcp客户端的相关接口
///
/// @version 1.0
/// @author lujun
/// @E-mail lujun.hust@gmail.com
/// @date 2011/08/21
//
//
// 修订说明:
//
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include "client.h"
// socket handle
int g_hSocket;
int connect_server( char *destIp, int destPort )
{
int result;
struct sockaddr_in address;
// create a socket
g_hSocket = socket(AF_INET,SOCK_STREAM,0);
// set server addr
address.sin_family = AF_INET;
// use server ip and listen port to connect
address.sin_addr.s_addr = inet_addr( destIp );
address.sin_port = htons(destPort);
// connect tcp server
result = connect(g_hSocket,(struct sockaddr *)&address,sizeof(address) );
if( result == -1 )
{
printf("[tcp client] can't connect server !\n");
return -1;
}
return 0;
}
int close_connect()
{
printf("close connect with server !\n ");
close(g_hSocket);
return 0;
}
int send_cmd( ServerPack sPack )
{
int recvBytes = 0;
int sendBytes = 0;
ReturnPack rPack;
// add cmd header
memcpy(sPack.cmdHeader,TCP_CMD_HEADER_STR,TCP_CMD_HEADER_LEN);
// send cmd
while(1)
{
sendBytes = send(g_hSocket,(uint8_t *)&sPack,SERVER_PACK_LEN,0);
if( sendBytes == SERVER_PACK_LEN )
{
printf("successfully send bytes %d\n",SERVER_PACK_LEN);
break;
}
else if( sendBytes <= 0 && errno != EINTR && errno != EWOULDBLOCK && errno != EAGAIN)
{
printf("disconnected or other errors!\n");
return -1;
}
else
{
continue;
}
}
// recv process result from server
while(1)
{
recvBytes = recv(g_hSocket,(uint8_t *)&rPack,RETURN_PACK_LEN,0);
if( recvBytes == RETURN_PACK_LEN )
{
break;
}
else if( recvBytes <=0 && errno != EINTR && errno != EWOULDBLOCK && errno != EAGAIN )
{
printf("disconnected or error occur!\n close the socket!\n");
return -1;
}
else
{
continue;
}
}
// check header
if ( memcmp( rPack.cmdHeader, TCP_CMD_HEADER_STR, TCP_CMD_HEADER_LEN ) != 0 )
{
printf("return pack header errror!\n");
return -2;
}
// get return status
if( rPack.returnCMD != DVS_RETURN_SUCCESS )
{
printf("return status : fail!\n");
return -3;
}
return 0;
}
三、服务器主线程代码
//
// COPYRIGHT NOTICE
// Copyright (c) 2011, 华中科技大学 ticktick(版权声明)
// All rights reserved.
//
/// @file server.c
/// @brief tcp服务器主线程代码
///
/// 实现tcp服务端监听线程相关函数
///
/// @version 1.0
/// @author lujun
/// @E-mail lujun.hust@gmail.com
/// @date 2011/08/21
//
//
// 修订说明:
//
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <unistd.h>
#include "serverIf.h"
int g_hServerSocket;
int open_port( int localport )
{
int result;
int clientSocket,client_len;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
// create a socket obj for server
g_hServerSocket = socket(AF_INET,SOCK_STREAM,0);
// bind tcp port
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(localport);
result = bind(g_hServerSocket,(struct sockaddr *)&server_addr,sizeof(server_addr) );
if( result != 0 )
{
printf("[tcp server] bind error!\n ");
return -1;
}
// begin to listen
result = listen(g_hServerSocket,5);
if( result != 0 )
{
printf("[tcp server] listen error!\n ");
return -1;
}
// 注: ServerEnv用于给服务线程传参,定义于serverIf.h中
ServerEnv env;
while(1)
{
client_len = sizeof(client_addr);
clientSocket = accept(g_hServerSocket,(struct sockaddr *)&client_addr,&client_len );
if( clientSocket < 0 )
{
printf("[tcp server] accept error!\n" );
return -1;
}
env.m_hSocket = clientSocket;
// add new tcp server thread
add_new_tcp_process_thr(&env);
}
return 0;
}
int close_port()
{
printf("close server port, stop listen!\n");
close(g_hServerSocket);
return 0;
}
四、服务器端服务线程代码
//
// COPYRIGHT NOTICE
// Copyright (c) 2011, 华中科技大学 ticktick(版权声明)
// All rights reserved.
//
/// @file serverIf.c
/// @brief tcp服务线程代码
///
/// 实现tcp服务线程相关接口
///
/// @version 1.0
/// @author lujun
/// @E-mail lujun.hust@gmail.com
/// @date 2011/08/21
//
//
// 修订说明:
//
#include "serverIf.h"
#include "../include/Command.h"
#include <sys/socket.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
int add_new_tcp_process_thr( ServerEnv *envp )
{
pthread_t tcpThr;
if( pthread_create( &tcpThr,NULL,tcpServerThrFxn,envp ) )
{
printf("tcp thread create fail!\n");
return -1;
}
printf("tcp thread has been created!\n");
return 0;
}
int save_user_name( char * pUsername )
{
printf("ok,user name saved,username=%s\n",pUsername);
return 0;
}
int save_user_age( int age )
{
printf("ok,user age saved,userage=%d\n",age);
return 0;
}
void * tcpServerThrFxn( void * arg )
{
ServerEnv * envp = (ServerEnv *)arg;
int socketfd = envp->m_hSocket;
int returnBytes;
ServerPack sPack;
ReturnPack rPack;
memcpy(rPack.cmdHeader,TCP_CMD_HEADER_STR,TCP_CMD_HEADER_LEN);
while(1)
{
// read cmd from client
returnBytes = recv(socketfd,(uint8_t *)&sPack,SERVER_PACK_LEN,0);
if( returnBytes == SERVER_PACK_LEN )
{
//printf("ok,recv %d bytes! \n",SERVER_PACK_LEN);
}
else if( returnBytes <= 0 && errno != EINTR && errno != EWOULDBLOCK && errno != EAGAIN )
{
printf("disconnected or error occur! errno=%d\n ",errno);
break;
}
else
{
continue;
}
// check the header
if ( memcmp( sPack.cmdHeader, TCP_CMD_HEADER_STR, TCP_CMD_HEADER_LEN ) != 0 )
{
rPack.returnCMD = DVS_RETURN_INVLID_HEADER;
// return error info to client
returnBytes = send(socketfd,(uint8_t *)&rPack,RETURN_PACK_LEN,0 ) ;
if( returnBytes < RETURN_PACK_LEN)
{
printf("send error!\n");
continue;
}
}
// analyse cmd
rPack.returnCMD = DVS_RETURN_SUCCESS;
switch( sPack.serverCMD )
{
case CMD_SAVE_USER_NAME:
{
if( save_user_name(sPack.Parameters.UserName.username) != 0)
{
rPack.returnCMD = DVS_RETURN_FAIL;
}
}
break;
case CMD_SAVE_USER_AGE:
{
if( save_user_age(sPack.Parameters.UserAge.userage) != 0)
{
rPack.returnCMD = DVS_RETURN_FAIL;
}
}
break;
default:
{
rPack.returnCMD = DVS_RETURN_INVLID_CMD;
}
break;
}
// return result info to client
returnBytes = send(socketfd,(uint8_t *)&rPack,RETURN_PACK_LEN,0 );
if( returnBytes < RETURN_PACK_LEN )
{
printf("send error!\n");
continue;
}
}
printf("close session socket!");
// close socket
close(socketfd);
return (void*)0;
}
五、客户端测试代码
//
// COPYRIGHT NOTICE
// Copyright (c) 2011, 华中科技大学 ticktick(版权声明)
// All rights reserved.
//
/// @file main.c
/// @brief tcp客户端测试代码
///
/// 实现 tcp客户端测试
///
/// @version 1.0
/// @author lujun
/// @E-mail lujun.hust@gmail.com
/// @date 2011/08/21
//
//
// 修订说明:
//
#include <stdio.h>
#include "client.h"
#define LOCAL_IP_STR "127.0.0.1"
#define DEST_IP_STR "192.201.0.8"
#define DEST_PORT 8000
int main(int argc, char **argv)
{
int i =0;
printf("tcp test start!\n");
if( connect_server(DEST_IP_STR,DEST_PORT) != 0)
{
return -1;
}
ServerPack sPack;
sPack.serverCMD = CMD_SAVE_USER_AGE;
sPack.Parameters.UserAge.userage = 20;
if( send_cmd(sPack) == -1 )
{
printf("send cmd fail!\n");
}
getchar();
getchar();
close_connect();
printf("tcp test pass!\n");
return 0;
}
六、服务器端测试代码
//
// COPYRIGHT NOTICE
// Copyright (c) 2011, 华中科技大学 ticktick(版权声明)
// All rights reserved.
//
/// @file main.c
/// @brief tcp客户端代码
///
/// 实现tcp服务器端测试的代码
///
/// @version 1.0
/// @author lujun
/// @E-mail lujun.hust@gmail.com
/// @date 2011/08/21
//
//
// 修订说明:
//
#include <stdio.h>
#include "server.h"
#include "serverIf.h"
#define LOCAL_PORT 8000
int main(int argc, char **argv)
{
printf("tcp test start!\n");
if( open_port(LOCAL_PORT) != 0)
{
return -1;
}
close_port();
printf(" close port !\n");
return 0;
}
七、总结和说明
本文后面的附件中有完整的代码,欢迎下载使用。编译方法,把代码文件夹都拷贝到linux下,在本代码文件夹的根目录下,运行make,
即可生成对应的可执行文件。在运行测试程序的时候,请先执行server.out,然后执行client.out
如果发现本代码的任何bug或者有任何建议,欢迎留言或者来信交流。