最近由於工作原因需要溫習一下Linux網絡編程的部分基礎知識,因此對之前寫的Socket網絡通信的代碼進行了進一步優化和拓展,在不關閉一次Socket連接的基礎上,對服務端加入循環讀寫的功能,同時加入連接檢測函數,防止客戶端意外斷開(例如Ctrl + C殺掉客戶端程序),服務端程序陷入死循環。增加客戶端退出命令(服務端亦接收此命令),增加客戶端執行服務端程序的命令,這裏主要用到了strncasecmp和system函數。
最近看同事代碼時看到了C程序中使用回調函數,爲了加強對回調函數的理解,於是簡單嘗試想用回調函數調用系統read和write函數,但後來發現第三個參數不兼容,編譯時告警(雖然可以通便編譯,正常運行),但最後只能作罷,將read函數和write的回調函數分別進行了編輯。關於回調函數,這篇博文寫的比較淺顯易懂,可以作爲入門講解:
https://www.cnblogs.com/jiangzhaowei/p/9129105.html
服務端程序:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include<netinet/tcp.h>
#define MAX_LINE 100
/* 處理函數,用於將大寫字符轉換爲小寫字符。參數爲需要轉換的字符串 */
void upper_to_lower(char *str)
{
if (str == NULL) /* 空串 */
return;
for (; *str != '\0'; str++)
{
if(*str >= 'A' && *str <= 'Z') /* 判斷字符並進行轉換,也可以使用庫函數 */
*str = *str -'A' + 'a';
}
}
/* 判斷接收的字符串是否爲定義的命令,若是則執行相應的操作 */
int msg_to_cmd(char *str)
{
int len = strlen(str);
if (strncasecmp(str, "excute", len) == 0)
{
cmd_excute("~/linux/helloWorld"); //執行指定目錄下的helloWorld應用程序
return -1;
}
else if (strncasecmp(str ,"quit", len) == 0)
{
return 1;
}
else
{
}
return 0;
}
/* 封裝Linux系統命令調用函數system */
int cmd_excute(char *cmd)
{
printf("Excute command: %s\n",cmd);
system(cmd);
return 0;
}
/*TCP連接檢測函數,用於檢測客戶端是否異常斷開,防止服務端socket程序崩潰*/
int connect_detect(int fd)
{
struct tcp_info info;
int len = sizeof(info);
if (fd <= 0) return 0;
getsockopt(fd, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *)&len);
/*if (info.tcpi_state == TCP_ESTABLISHED) */
if (info.tcpi_state == 1) // Is connected
return 0;
else // Losed
return 1;
}
/*封裝write函數的回調函數,增加打印功能 */
int write_callback(int *fd, const void *buf_ptr, size_t len, ssize_t (*wr)(int, const void*, size_t))
{
int res = 0;
res = wr(*fd, buf_ptr, len);
if (res != -1)
{
printf("Write message succeed !\n");
}
else
{
printf("Write message failed !\n");
}
//res = wr(*fd, buf_ptr, len);
//printf("Receive Message: %s\n", buf_ptr);
return res;
}
/* 由於read函數的第二個參數非const,因此無法與write函數使用同一回調函數,否則編譯器會報警 */
int read_callback(int *fd, void *buf_ptr, size_t len, ssize_t (*rd)(int, void*, size_t))
{
int count = 0;
count = rd(*fd, buf_ptr, len);
printf("Receive message: %s\n", buf_ptr);
return count;
}
int main(void)
{
struct sockaddr_in sin;
struct sockaddr_in cin;
int l_fd;
int c_fd;
socklen_t len;
char buf[MAX_LINE]; /* 存儲傳送內容的緩衝區 */
char addr_p[INET_ADDRSTRLEN]; /* 存儲客戶端地址的緩衝區 */
int port = 80000; /* 端口號,使用8000 */
bzero(&sin, sizeof(sin)); /* 清空地址結構 */
sin.sin_family = AF_INET; /* 使用IPv4通信域 */
sin.sin_addr.s_addr = INADDR_ANY; /* 服務器可以接受任意地址 */
sin.sin_port = htons(port); /* 端口號轉換爲網絡字節序 */
l_fd = socket(AF_INET, SOCK_STREAM, 0); /* 創立套接字,使用TCP協議 */
bind(l_fd, (struct sockaddr*) &sin, sizeof(sin)); /* 將地址和套接字綁定 */
listen(l_fd, 10); /* 開始監聽連接請求 */
//printf("Waiting ...\n");
while (printf("Waiting ...\n"))
{/* 服務器程序多半是死循環 */
/* 接受連接請求,從此函數中返回後就可以開始通信了 */
int cmd_result = 0;
//printf("Waiting ...\n");
c_fd = accept(l_fd, (struct sockaddr*) &cin, &len);
while (1)
{
/*
if (recv(c_fd, buf, MAX_LINE, 0) == -1){
break;
}
count = read(c_fd, buf, MAX_LINE);
*/
/* 讀取客戶端傳來的信息 */
//inet_ntop(AF_INET, &cin.sin_addr, addr_p, sizeof(addr_p));
/* 將客戶端地址轉換爲字符串 */
//printf("client IP is %s, port is %d\n", addr_p, ntohs(cin.sin_port));
/* 打印客戶端地址和端口號 */
//printf("Receive Message: %s\n", buf);
/* 打印客戶端發送過來的字符串 */
int wrt = 0;
read_callback(&c_fd, buf, MAX_LINE, read);
upper_to_lower(buf); /* 調用大小寫轉換函數 */
cmd_result = msg_to_cmd(buf);
if( (cmd_result == 1 ) || connect_detect(c_fd) ) /*若客戶端執行quit命令正常退出或者客戶端異常斷開則跳出本次循環*/
{
printf("End communication !\n");
break;
}
else if ( cmd_result == -1 )
{
char *p = "Command excuted succeed !";
wrt = write_callback(&c_fd, p, 25, write);
}
else
{
wtr = write_callback(&c_fd, buf, strlen(buf)+1, write); /* 將轉換後的字串發給客戶端 */
}
if ( wrt == -1 )
{
printf("End communication !\n");
break;
}
}
close(c_fd); /* 通信結束,關閉套接字,準備下一次通信 */
}
if(close(l_fd) == -1)
{ /* 通信結束,關閉套接字,準備下一次通信 */
perror("Fail to close");
exit(1);
}
return 0; /* 不應該執行到這裏 */
}
在服務端目錄~/linux/下面創建並編譯helloWorld.c文件,在監聽程序中由system函數執行運行helloWorld程序。
客戶端程序:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/types.h>
#include <ctype.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#define MAX_LINE 100
int cmd_excute(char *cmd)
{
printf("Excute command: %s\n",cmd);
system(cmd);
return 0;
}
int msg_to_cmd(char *str)
{
int len = strlen(str);
if (strncasecmp(str ,"quit", len) == 0)
{
return 1;
}
else if (strncasecmp(str, "ls", len) == 0)
{
cmd_excute("ls ./");
return 2;
}
else if(strncasecmp(str ,"help", len) == 0)
{
printf("Command list:\n");
printf("ls --display directory\n");
printf("quit --End communication\n");
return 3;
}
else
{
}
return 0;
}
int main(void)
{
struct sockaddr_in sin;
char msg[100];
int l_fd;
char buf[MAX_LINE]; /* 存儲傳送內容的緩衝區 */
int port = 80000; /* 端口號,使用8000 */
bzero(&sin, sizeof(sin)); /* 清空地址結構 */
sin.sin_family = AF_INET; /* 使用IPv4通信域 */
inet_pton(AF_INET,"10.17.39.166", &sin.sin_addr);
sin.sin_port = htons(port); /* 端口號轉換爲網絡字節序 */
l_fd = socket(AF_INET, SOCK_STREAM, 0); /* 創立套接字,使用TCP協議 */
connect(l_fd, (const struct sockaddr*)&sin, sizeof(sin));
while(1)
{
int wrt = -1;
int res = 0;
int flag = 0;
// l_fd = socket(AF_INET, SOCK_STREAM, 0); /* 創立套接字,使用TCP協議 */
// connect(l_fd, (const struct sockaddr*)&sin, sizeof(sin));
printf("Input Message:\n");
scanf("%s", msg);
res = msg_to_cmd(msg);
switch(res){
case 0:
wrt = write(l_fd, msg, strlen(msg)+1);
sleep(3);
if ( wrt != -1 )
{
printf("Write message succeed !\n");
}
else
{
printf("Write message failed !\n");
flag = 1;
break;
}
read(l_fd, buf, MAX_LINE);
printf("Receive messaege from server:\n%s\n", buf);
break;
case 1:
res = write(l_fd, msg, strlen(msg)+1);
sleep(3);
close(l_fd);
flag = 1;
break;
case 2:
break;
default:
break;
}
if (flag)
{
break;
}
// printf("Socket state: %#X\n", l_fd);
}
if(close(l_fd) == -1){ /* 通信結束,關閉套接字,準備下一次通信 */
perror("Fail to close !");
}
return 0;
}