网络编程 期中_动手编个小程序
一、题目
请分别写一个客户端程序和服务器程序,客户端程序连接上服务器之后,通过敲命令和服务器进行交互,支持的交互命令包括:
- 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
左:客户端;右:服务端