網絡編程 期中_動手編個小程序
一、題目
請分別寫一個客戶端程序和服務器程序,客戶端程序連接上服務器之後,通過敲命令和服務器進行交互,支持的交互命令包括:
- pwd:顯示服務器應用程序啓動時的當前路徑
- cd:改變服務器應用程序的當前路徑
- ls:顯示服務器應用程序當前路徑下的文件列表
- quit:客戶端進程退出,但是服務器端不能退出,第二個客戶可以再次連接上服務器端
客戶端程序要求
- 可以指定待連接的服務器端 IP 地址和端口
- 在輸入一個命令之後,回車結束,之後等待服務器端將執行結果返回,客戶端程序需要將結果顯示在屏幕上
服務器程序要求
- 暫時不需要考慮多個客戶併發連接的情形,只考慮每次服務一個客戶連接
- 要把命令執行的結果返回給已連接的客戶端
- 服務器端不能因爲客戶端退出就直接退出
二、開始
客戶端
telnet-client.c
#include "common.h"
int main(int argc, char **argv) {
// 客戶端執行命令格式:./telnet-client 127.0.0.1 43211
// 看格式就知道有 3 個參數,後兩個依次是 IP 地址、端口號(服務器定義的),滿足題目關於客戶端的第一個要求
if (argc != 3) {
error(1, 0, "usage: telnet-client IPaddress port");
}
// 獲取端口號(將 ASCII 轉換爲整數)
int port = atoi(argv[2]);
// 創建套接字(TCP:SOCK_STREAM)
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
// 創建 IPV4 套接字地址格式(含 IP 地址、端口號)
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(port);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
// 當前是 TCP,調用 connect 函數將激發 TCP 三次握手
int connect_rt = connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr);
if (connect_rt < 0) {
error(1, errno, "connect failed");
}
char send_line[MAXLINE];
char recv_line[MAXLINE];
fd_set allreads;
fd_set readmask;
FD_ZERO(&allreads);
FD_SET(0, &allreads);
FD_SET(sockfd, &allreads);
for (;;) {
readmask = allreads;
// 使用 select 同時處理標準輸入和套接字
int rt = select(sockfd + 1, &readmask, NULL, NULL, NULL);
if (rt <= 0) {
error(1, errno, "select failed");
}
// 套接字可讀事件
if (FD_ISSET(sockfd, &readmask)) {
// 讀數據
int n = read(sockfd, recv_line, MAXLINE);
if (n < 0) {
error(1, errno, "read failed");
} else if (n == 0) {
printf("server closed\n");
break; // 跳出 for 循環,然後通過 exit(0) 退出進程
}
recv_line[n] = 0;
fputs(recv_line, stdout);
fputs("\n", stdout);
}
// 標準輸入事件(STDIN_FILENO:標準輸入文件描述符,值爲 0)
if (FD_ISSET(STDIN_FILENO, &readmask)) {
if (fgets(send_line, MAXLINE, stdin) != NULL) {
int i = strlen(send_line);
if (send_line[i - 1] == '\n') {
send_line[i - 1] = 0;
}
// 讀到 quit,調用 shutdown 函數關閉發送方向
if (strncmp(send_line, "quit", strlen(send_line)) == 0) {
if (shutdown(sockfd, 1)) {
error(1, errno, "shutdown failed");
}
}
// 發送讀到的內容
if (write(sockfd, send_line, strlen(send_line)) < 0) {
error(1, errno, "write failed");
}
}
}
}
exit(0);
}
服務端
telnet-server.c
#include "common.h"
static int count;
static void sig_int(int singo) {
printf("\nreceived %d datagrams\n", count);
exit(0);
}
char *run_cmd(char *cmd) {
char *data = malloc(16 * 1024);
bzero(data, sizeof(data));
char *data_index = data; // 通過移動指針 data_index 間接操作 data,最終可以直接返回 data,因爲 data 的指針沒有移動
const int max_buffer = 256;
char buffer[max_buffer];
// 這裏使用 popen,別搞錯了,不是 fopen 或 open
FILE *fp = popen(cmd, "r"); // 若成功打開,則必須調用 pclose 關閉
if (fp) {
while (!feof(fp)) {
if (fgets(buffer, sizeof(max_buffer), fp) != NULL) {
int len = strlen(buffer);
memcpy(data_index, buffer, len);
data_index += len;
}
}
pclose(fp);
fp = 0;
}
return data;
}
int main(int argc, char **argv) {
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
// 在 bind 之前設置 SO_REUSEADDR 套接字選項纔有效
int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
int bind_rt = bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (bind < 0) {
error(1, errno, "bind failed");
}
int listen_rt = listen(listenfd, LISTENQ);
if (listen_rt < 0) {
error(1, errno, "listen failed");
}
signal(SIGPIPE, SIG_IGN);
int connfd;
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
char buf[256];
count = 0;
// accept 在 for(;;) 循環中,阻塞的,一個一個響應
while (1) {
if ((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0) {
error(1, errno, "accept failed");
}
while (1) {
bzero(buf, sizeof(buf));
int n = read(connfd, buf, sizeof(buf));
if (n < 0) {
error(1, errno, "read failed");
} else if (n == 0) {
printf("client closed\n");
close(connfd);
break;
}
count++;
buf[n] = 0;
if (strncmp(buf, "ls", 2) == 0) {
char *result = run_cmd("ls");
if (send(connfd, result, strlen(result), 0) < 0) {
return 1;
}
free(result);
} else if (strncmp(buf, "pwd", 3) == 0) {
char buf[256];
char *result = getcwd(buf, 256);
if (send(connfd, result, strlen(result), 0) < 0) {
return 1;
}
} else if (strncmp(buf, "cd ", 3) == 0) { // 注意:cd 後面有一個空格
char target[256];
bzero(target, sizeof(target));
memcpy(target, buf + 3, strlen(buf) - 3);
if (chdir(target) == -1) {
printf("change dir failed, %s\n", target);
}
} else {
char *error = "error: unknow input type";
if (send(connfd, error, strlen(error), 0) < 0) {
return 1;
}
}
}
}
exit(0);
}
頭文件 common.h
#ifndef MID_TEST_COMMON_H
#define MID_TEST_COMMON_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/socket.h> /* basic socket definitions */
#include <netinet/in.h> /* sockaddr_in{} and other Internet defns */
#include <arpa/inet.h> /* inet(3) functions */
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <sys/select.h>
void error(int status, int err, char *fmt, ...);
#define SERV_PORT 43211
#define MAXLINE 4096
#define LISTENQ 1024
#endif //MID_TEST_COMMON_H
三、CMake 管理當前項目
① 代碼組成
-CMakeLists.txt
-include:存放頭文件
-src:存放源代碼
CMakeLists.txt
CMAKE_MINIMUM_REQUIRED(VERSION 3.1)
SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)
INCLUDE_DIRECTORIES(${CMAKE_SOURCE_DIR}/include)
ADD_SUBDIRECTORY(src)
include 目錄:include/common.h(common.h 上面有)
src 目錄(telnet-client.c、telnet-server.c 上面有)
src/CmakeLists.txt
ADD_EXECUTABLE(telnet-client telnet-client.c)
TARGET_LINK_LIBRARIES(telnet-client)
ADD_EXECUTABLE(telnet-server telnet-server.c)
TARGET_LINK_LIBRARIES(telnet-server)
② 創建並進入 build 目錄
mkdir build && cd build
③ 外部編譯
cmake .. && make
四、測試
測試步驟
① 打開兩個命令行窗口
② 其中一個窗口先執行服務器命令,輸入命令 ./telnet-server
後回車
③ 另一個窗口再執行客戶端命令,輸入命令 ./telnet-client 127.0.0.1 43211
後回車
在客戶端所在命令行窗口
輸入 ls、pwd、cd xxx(xxx 代表目錄)命令,服務器正常返回結果
輸入 quit,客戶端退出,服務器打印 client closed
左:客戶端;右:服務端