- 所谓的ECHO服务就是在屏幕打印相关的参数,相关的应用过程如下:
服务器逻辑
- 1.服务器启动之后创建服务器socket,进行相应的设置后始终调用accept(2)等待客户端的连入,客户端 正常连入之后创建一个子进程作为业务进程,对客户端进行服务,父进程 始终作为监听进程等待下一个客户端的连入;
- 2.其中为了防止僵尸进程的出现,服务器需要有处理子进程退出的功能,简便起见,程序中直接安装一个信号处理程序,用于处理SIGCHLD信号,这个过程完全异步,没有体现在流程图内。
如图为程序流程图
但是该服务器的逻辑比较简单,只能实现对一个客户端的单次连接,在连接后向客户端发出一定的信息,然后断开连接
整理socket相关操作的函数原型:
int socket(int domain, int type, int protocol);//创建socket
//sock_fd = socket(AF_INET, SOCK_STREAM, 0);
//sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
int bind(int socket, const struct sockaddr *address, socklent address_len);//绑定地址端口,绑定 成功则 绑定失败则
int connect(int socket, const struct sockaddr *address, socklent address_len);//连接
//可以看出,其中的参数完全相同,
//bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr))
int listen(int socket, int backlog);//监听模式 backlog 是指等待连接的队列长度,但是实际上的队列可能会大于这个数字,通常都取 5。
int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);//接受连接
代码解析:
- 1.服务器相关代码:
- 创建socket:server_sock = socket(AF_INET, SOCK_STREAM, 0);
int server_sock,conn_sock;
server_sock = socket(AF_INET,SOCK_STREAM,0); //功能相关
if(server_sock<0){ //处理错误
perror("socket(2) erros");
goto create_error;
//main内定义create_error内容
}
- 2 绑定到端口:bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_add))
struct sockaddr_in server_addr,client_addr;
//IP 协议使用的地址描述数据结构,定义在头文件<netinet/in.h>中。
socklen_t sock_len = sizeof(client_addr);
(void)memset(&server_addr,0,sock_len);//函数的功能是:将指针变量 s 所指向的前 n 字节的内存单元用一个“整数” c 替换,注意 c 是 int 型。s 是 void* 型的指针变量,所以它可以为任何类型的数据进行初始化。
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(LISTEN_PORT);
if(bind(server_sock,(struct sockaddr *)&server_addr,sizeof(server_add))){
perror("bind(2) error");
goto err;
}
绑定的地址和端口主要是在 57 到 59 行填充 sturct sockaddr_in 结构完成的,服务器没有
特殊要求的情况下,绑定地址用 INADDR_ANY 监听所有地址即可,另外要注意字节序的转
换,这对于程序,尤其是对可移植性有要求的程序是一定要注意的。
- 设置为被动监听:
if(listen(server_sock,5)){
perror("listen(2) error");
goto err;
}
- 接受新的连接:
while(true){
sock_len = sizeof(client_addr);
conn_sock = accept(server_sock,(struct sockaddr *)&client_addr,&sock_len);
if (conn_sock < 0) {
if (errno == EINTR) {
/* restart accept(2) when EINTR */
continue;
}
goto end;
}
printf("client from %s:%hu connected\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
fflush(stdout);//清除所有的输入输出文件内容
- 子进程连接客户端:
chld_pid = fork(); //通过fork创建子进程
if (chld_pid < 0) {//小于0则表明创建失败
/* fork(2) error */
perror("fork(2) error");
close(conn_sock);
goto err;
} else if (chld_pid == 0){//为0则代表返回子进程
/* child process */
int ret_code;
close(server_sock);
ret_code = tcp_echo(conn_sock);
close(conn_sock);
/* Is usage of inet_ntoa(2) right? why? */
printf("client from %s:%hu disconnected\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
exit(ret_code); //exit(x)(x不为0)都表示异常退出 exit(0)表示正常退出 exit()的参数会被传递给一些操作系统,包括UNIX,Linux,和MS DOS,以供其他程序使用。
} else {
/* parent process */
continue;
}
执行调用之后,后面的代码
就分为父子两个进程继续运行。我们在这里让子进程去对新连接的客户端提供服务,服务完
成后就退出。父进程则继续进行监听,等待下一个客户端的连接。这样,服务器就可以并发
的对多个客户端进行服务响应。
子进程首先调用 close(2)关闭自己的监听 Socket,然后调用 tcp_echo()函数进行
服务。服务完成后关闭 Socket,打印客户端断开连接信息后退出进程。
- 服务函数
int tcp_echo(int client_fd)
{
char buff[BUFF_SIZE] = {0};
ssize_t len = 0;
len = read(client_fd, buff, sizeof(buff));
if (len < 1) {
goto err;
}
(void)write(client_fd, buff, (size_t)len);
return EXIT_SUCCESS;
err:
return EXIT_FAILURE;
/*
EXIT_SUCCESS是C语言头文件库中定义的一个符号常量。return EXIT_SUCCESS相当于return 0;return EXIT_FAILURE相当于return 1; 这样写有什么好处呢,程序有易读性吗?
头文件stdlib.h中:#include <stdlib.h>
Definition of the argument values for the exit() function
#define EXIT_SUCCESS 0
#define EXIT_FAILURE 1
*/
}
- SIGCHLD信号处理函数,防止僵尸进程:
所有的子进程在处理完毕服务之后,会直接调用 exit(3)终止自己。在这个
时候,系统会保留他们返回的终止状态,并发送 SIGCHLD 信号给父进程,同时子进程进入
僵尸态。只有父进程处理了之后资源才能真正地完全回收。所以可用如下这样一个函数来实
现对僵尸子进程的回收处理。
void zombie_cleaning(int signo)
{
int status;
(void)signo;
while (waitpid(-1, &status, WNOHANG) > 0);
}
- 安装信号 处理函数
struct sigaction clean_zombie_act;
(void)memset(&clean_zombie_act, 0, sizeof(clean_zombie_act));
clean_zombie_act.sa_handler = zombie_cleaning;
if (sigaction(SIGCHLD, &clean_zombie_act, NULL) < 0) {
perror("sigaction(2) error");
goto err;
}
客户端程序:
- 启动后立即创建 Socket,并且直接调用 connect(2) 连接服务器,省去 bind(2)调用,系统会将刚才创建的 Socket 隐式绑定到一个随机端口上。connect(2)后直接发送数据到服务器,发送完毕后直接读取服务器回发的数据并打印接收到的数据,然后结束
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#define SERVER_IP "192.168.1.133"
#define SERVER_PORT ((uint16_t)7007)
#define BUFF_SIZE (1024 * 4)
int main(int argc, char *argv[])
{
int conn_sock;
char test_str[BUFF_SIZE] = "tcp echo test";
struct sockaddr_in server_addr;
conn_sock = socket(AF_INET, SOCK_STREAM, 0);
if (conn_sock < 0) {
perror("socket(2) error");
goto create_err;
}
(void)memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
if (argc != 3) {
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);//inet_addr()用来将参数cp 所指的网络地址字符串转换成网络所使用的二进制数字. 网络地址字符串是以数字和点组成的字符串, 例如:"163. 13. 132. 68".
} else {
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
snprintf(test_str, BUFF_SIZE, "%s", argv[2]);
}
if (connect(conn_sock,
(struct sockaddr *)&server_addr,
sizeof(server_addr)) < 0) {
perror("connect(2) error");
goto err;
}
if (write(conn_sock, test_str, strlen(test_str)) < 0) {
perror("send data error");
goto err;
}
(void)memset(test_str, 0, BUFF_SIZE);
if (read(conn_sock, test_str, BUFF_SIZE) < 0) {
perror("receive data error");
goto err;
}
printf("%s\n", test_str);
fflush(stdout);//清空文件缓冲区或者标准输入输出缓冲区
return EXIT_SUCCESS;
err:
close(conn_sock);
create_err:
fprintf(stderr, "client error");
return EXIT_FAILURE;
}
服务器代码:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <signal.h>
#define LISTEN_PORT ((uint16_t)7007)
#define BUFF_SIZE (1024 * 4)
void zombie_cleaning(int signo)
{
int status;
(void)signo;
while (waitpid(-1, &status, WNOHANG) > 0);
}
int tcp_echo(int client_fd)
{
char buff[BUFF_SIZE] = {0};
ssize_t len = 0;
len = read(client_fd, buff, sizeof(buff));
if (len < 1) {
goto err;
}
(void)write(client_fd, buff, (size_t)len);
return EXIT_SUCCESS;
err:
return EXIT_FAILURE;
/*
EXIT_SUCCESS是C语言头文件库中定义的一个符号常量。return EXIT_SUCCESS相当于return 0;return EXIT_FAILURE相当于return 1; 这样写有什么好处呢,程序有易读性吗?
头文件stdlib.h中:#include <stdlib.h>
Definition of the argument values for the exit() function
#define EXIT_SUCCESS 0
#define EXIT_FAILURE 1
*/
}
int main(void)
{
int server_sock, conn_sock; //连接的定义
struct sockaddr_in server_addr, client_addr;
socklen_t sock_len = sizeof(client_addr);
pid_t chld_pid;
struct sigaction clean_zombie_act;
server_sock = socket(AF_INET, SOCK_STREAM, 0);
if (server_sock < 0) {
perror("socket(2) error"); //C 库函数 void perror(const char *str) 把一个描述性错误消息输出到标准错误 stderr。首先输出字符串 str,后跟一个冒号,然后是一个空格。
goto create_err;
}
(void)memset(&server_addr, 0, sock_len);
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(LISTEN_PORT);
if (bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr))) {
perror("bind(2) error");
goto err;
}
if (listen(server_sock, 5)) {
perror("listen(2) error");
goto err;
}
(void)memset(&clean_zombie_act, 0, sizeof(clean_zombie_act));
clean_zombie_act.sa_handler = zombie_cleaning;
if (sigaction(SIGCHLD, &clean_zombie_act, NULL) < 0) {
perror("sigaction(2) error");
goto err;
}
while (true) {
sock_len = sizeof(client_addr);
conn_sock = accept(server_sock, (struct sockaddr *)&client_addr, &sock_len);
if (conn_sock < 0) {
if (errno == EINTR) {
/* restart accept(2) when EINTR */
continue;
}
goto end;
}
printf("client from %s:%hu connected\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
fflush(stdout);
chld_pid = fork(); //通过fork创建子进程
if (chld_pid < 0) {//小于0则表明创建失败
/* fork(2) error */
perror("fork(2) error");
close(conn_sock);
goto err;
} else if (chld_pid == 0){//为0则代表返回子进程
/* child process */
int ret_code;
close(server_sock);
ret_code = tcp_echo(conn_sock);
close(conn_sock);
/* Is usage of inet_ntoa(2) right? why? */
printf("client from %s:%hu disconnected\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
exit(ret_code); //exit(x)(x不为0)都表示异常退出 exit(0)表示正常退出 exit()的参数会被传递给一些操作系统,包括UNIX,Linux,和MS DOS,以供其他程序使用。
} else {
/* parent process */
continue;
}
}
end:
perror("exit with:");
close(server_sock);
return EXIT_SUCCESS;
err:
close(server_sock);
create_err:
fprintf(stderr, "server error");
return EXIT_FAILURE;
}