• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

UNIX网络编程| 03TCP客户/服务器程序展示

武飞扬头像
Jxiepc
帮助1

1、概述

【简易回射服务器】:
- 客户从标准输入读入一行文本,并写给服务器;
- 服务器从网络输入读入这行,并回射给客户;
- 客户从网络输入读入这行回射文件,并显示在标准输出上;

学新通

2、TCP回射服务器程序

创建套接字,捆绑服务器的众所周知端口

创建一个套接字,在待捆绑到该TCP套接字的网际网套接字地址结构中填入通配地址和服务器的端口;

等待完成客户连接

服务器阻塞与accept调用,等待客户连接的完成;

并发服务器

fork为每个客户派生一个处理它们的子进程;子进程关闭监听套接字,父进程关闭已连接套接字;
子进程接着调用str_echo处理客户;

echo读入缓冲区并回射其中内容

read函数从套接字读入数据,writen函数把其中内容回射给客户;
若客户关闭连接,则接收到客户的FIN将导致服务器子进程的read函数返回0,即又导致str_echo函数的返回,终止子进程;
PORT为9877

void str_echo(int sockfd) {
    ssize_t n;
    char buf[MAXLINE];

    again:
    while ((n = read(sockfd, buf, MAXLINE)) > 0)
        write(sockfd, buf, n);
    if(n < 0 && errno == EINTR)
        goto again;
    else if (n < 0) {
        err_sys("str_echo: read error");
    }
}

int main() {
    int lfd, cfd;

    pid_t pid;
    socklen_t clen;
    struct sockaddr_in s_addr, c_addr;

    lfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&s_addr, sizeof(s_addr));
    s_addr.sin_family = AF_INET;
    s_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    s_addr.sin_port = htons(SERV_PORT);

    bind(lfd, (struct sockaddr*)&s_addr, sizeof(s_addr));

    listen(lfd, LISTENQ);

    while (1) {
        clen = sizeof(c_addr);
        cfd = accept(lfd, (struct sockaddr*)&c_addr, &clen);
        if((pid = fork()) == 0) {
            close(lfd);
            str_echo(cfd);
            exit(0);
        close(cfd);
    }

    return 0;
}
学新通

3、TCP回射客户程序

读入一行,使用write写到服务器,在使用readline将其从服务器读回;
void str_cli(FILE *fp, int sockfd) {
    char sendline[MAXLINE], recvline[MAXLINE];
    while (fgets(sendline, MAXLINE, fp) != NULL) {
        write(sockfd, sendline, strlen(sendline));
        if(Readline(sockfd, recvline, MAXLINE) == 0)
            err_quit("str_cli: server terminated prematurely");
        fputs(recvline, stdout);
    }
}

int main(int argc, char *argv[]) {
    int sockfd, n;
    char recvline[MAXLINE   1];

    struct sockaddr_in servaddr;

    /* 创建套接字 */
    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
        err_sys("socket eror");

    /* 构建结构体ip、port */
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    if(inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
        err_quit("inet_pton error");

    /* 建立连接 */
    int c = connect(sockfd, (const struct sockaddr*)&servaddr, sizeof(servaddr));
    if(c == -1) {
        err_sys("connnect error");
    }
    str_cli(stdin, sockfd);

    return 0;
}
学新通

学新通
学新通

3、正常启动

启动服务器将其置于后台中运行;

学新通

查看服务器监听套接字的状态;处于LISTEN状态

学新通

- 客户调用str_cli函数,该函数将阻塞于fgets调用,因为我们还未曾键入过一行文本;
- 当服务器中的accept返回时,服务器调用fork,再由子进程调用str_echo。该函数调用readline,readline调用read,而read在等
	待客户送入一行文本期间阻塞。
- 另一方面,服务器父进程再次调用accept并阻塞,等待下一个客户连接。
至此,我们有3个都在睡眠(即已阻塞)的进程:客户进程、服务器父进程和服务器子进程。
当客户连接到服务器上时;
一个父进程仍处于LISTEN,等待新的客户;而子进程已进入ESTABLISHED;
客户进程的本地端口号为37340;

学新通

使用ps命令查看进程状态和关系;
【WCHAN】:为睡眠状态时的条件;
	- n_tty_read:阻塞读取;
	- sk_wait_data:阻塞等待发送数据;
	- inet_csk_accept:阻塞等待连接;
【STAT】:进程的状态;

学新通

4、正常终止

在命令行中,使用<Ctrl D>即可键入终端EOF字符,fget()条件不满足即退出;
使用netstat 查看client处于close_wait状态;

学新通
终止步骤

- 当我们键入EOF字符时,fgets返回一个空指针,于是str_cli函数返回。
- 当str_cli返回到客户的main函数时,main通过调用exit终止;
- 进程终止处理的部分工作是关闭所有打开的描述符,因此客户打开的套接字由内核关闭。这导致客户TCP发送一个FIN给服务
	器,服务器TCP则以ACK响应,这就是TCP连接终止序列的前半部分。至此,照务器套接字处于CLOSE_WAIT状态,客户套接
	字则处于FIN_WAIT_2状态;
- 当服务器TCP接收FIN时,服务器子进程阻塞于readline调用,于是readline返回0。这导致str_echo函数返回服务器子进程的main函数;
- 服务器子进程通过调用exit来终止;
- 服务器子进程中打开的所有描述符随之关闭,由子进程来关闭已连接套接字会引发TCP连接终止序列的最后两个分节:
	一个从服务器到客户的FIN和一个从客户到服务器的ACK。至此,连接完全终止,客户套接字进入TIME_WAIT状态;
- 进程终止处理的另一部分内容是:在服务器子进程终止时,给父进程发送一个SIGCHLD信号。这一点在本例中发生了,但是我们
	没有在代码中捕获该信号,而该信号的默认行为是被忽略。既然父进程未加处理,子进程于是进入僵死状态,我们可以使用ps命令验证这一点;

学新通

5、POSIX信号处理

可参考此处

信号就是告知某个进程发生了某个事件的通知,有时也称为软件中断。信号通常是异步发生的,也就是说进程预先不知道信号的准确发生时刻。
- 由一个进程发给另一个进程(或自身);
- 由内核发给某个进程。
通过sigaction来设定一个信号的处置:
- 我们可以提供一个信号处理函数,只要有特定信号发生它就被调用——捕获信号;
	有两个信号不能被捕获,STGKILL和STGSTOP;
	信号处理函数由信号值这个单一的整数参数来调用,且没有返回值(void handler(int signo));
- 信号的处置设定为SIG_IGN来忽略它;
- 可把信号的处置设定为SIG_DFL来启用默认处置,收到信号后终止进程,其中某些信号还在当前工作目录产生一个进程的核心映
	像,另有个别信号的默认处置是忽略,SIGCHLD和sIGURG就是本书中出现的默认处置为忽略的两个信号;

5.1 signal函数

Sigfunc *signal(int signo, Sigfunc *func) {
    struct sigaction act, oact;
    act.sa_handler = func;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    if(signo == SIGALRM) {
#ifdef SA_INTERRUPT
        act.sa_flags |= SA_INTERRUPT;
#endif
    } else {
#ifdef SA_RESTART
        act.sa_flags |= SA_RESTART;
#endif
    }
    if(sigaction(signo, &act, &oact) < 0)
        return SIG_ERR;
    return oact.sa_handler;
}
学新通

使用typedef简化函数原型

// 原型为
void (*signal(int signo, void (*func)(int)))(int);

typedef void Sigfunc(int);

// 简化为
Sigfunc *signal(int signo, Sigfunc *func);

设置处理函数

设置sigaction中的sa_handler为func参数;

设置SA_RESTART

若设置,由相应信号中断的系统调用将由内核重启;
若被捕获的信号SIGALRM且SA_RESTRT由定义,则需要设置该标志;

调用sigaction函数

调用sigaction函数,并将相应信号的旧行为作为signal函数的返回值;

5.2 POSIX信号语义

- 一旦安装了信号处理函数,它便一直安装着;
- 在一个信号处理函数运行期间,正被递交的信号是阻塞的。而且,安装处理函数时在传递给sigaction函数的sa_mask信号集中指
	定的任何额外信号也被阻塞。
- 如果一个信号在被阻塞期间产生了一次或多次,那么该信号被解阻塞之后通常只递交一次,也就是说Unix信号默认是不排队的;
- 利用sigprocmask函数选择性地阻塞或解阻塞一组信号是可能的;这使得我们能够在一段临界区代码执行期间,防止捕获某些信号,来保护这段代码;

6、处理SIGCHID信号

可参考该文章

6.1 处理僵尸进程

以防出现僵尸进程,建立一个SIGCHLD信号的信号处理函数,将其添加在listen后;
且该还能必须在第一个子进程之前完成,且只做一次
void sig_chld(int signo) {
	pid_t pid;
	int stat;
	pid = wait(&stat);
	printf("child %d ternimated\n", pid);
	return;
}

将信号添加到服务端代码后将不在有僵尸进程
学新通

与之前相比,此时查看进程不在有僵尸进程;

学新通
具体步骤

- 键入EOF字符来终止客户端。客户TCP发送一个FIN给服务器,服务器响应以一个ACK;
- 收到客户的FIN导致服务器TCP递送一个EOF给子进程阻塞中的readline,从而子进程终止;
- 当SIGCHLD信号递交时,父进程阻塞于accept调用。sig_chld函数执行,其wait调用取到子进程的PID和终止状态,随后是printf调用,最后返回;
- 若信号是在父进程阻塞于慢系统调用(accept)时由父进程捕获的,内核就会使accept返回一个EINTR错误,而父进程不处理该错误,故中止;

6.2 处理被中断的系统调用

系统慢调用规则

当阻塞于慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用返回一个EINTR错误;
【移植性】:
- 我们编写捕获信号的程序时,必须对慢系统调用返回EINTR错处理;若某实现支持SA_RESTART标志,并非所有被中断系统调
	用都可自动重启(如源自Berkeley);

处理中断的accept

/**
以下做法对于read、write、select、open都适用;
而对于connect被一个捕获的信号中断且不自动重启时,必须调用selecy来等待连接完成;
*/ 
while(1) {
	clen = sizeof(c_addr);
	if(cfd = accept(lfd, (struct sockaddr*)&c_addr, &clen) < 0) {
		if(errno == EINER)
			continue;
		else 
			err_sys("accept error");
	}
}

7、wait和waitpid函数

Linux系统编程 | 【03】进程、环境变量、IPC

#include <sys/wait.h>

pid_t wait(int *statloc);
pid_t waitpit(pid_t pid, int *statloc, int options);
/**
上述两个函数均返回两个值:已终止子进程的进程ID号,以及通过statloc指针返回的子进程终止状态;
可通过三个宏来检测终止状态,并辨别子进程时正常终止、由某个信号杀死还是仅由作业控制停止;
- 若调用wait的进程没有已终止的子进程,不过有一个或多个子进程仍在执行,那么wait将阻塞到现有子进程第一个终止为止;
- 具体参数看上述文章;
*/

7.1 wait和wait的区别

学新通

int main(int argc, char *argv[]) {
    int sockfd[5];
    struct sockaddr_in servaddr;
    for (int j = 0; j < 5;   j) {
        sockfd[j] = socket(AF_INET, SOCK_STREAM, 0);
        bzero(&servaddr, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(SERV_PORT);
        inet_pton(AF_INET, argv[1], &servaddr);
        connect(sockfd[1], (struct sockaddr*)&servaddr, sizeof(servaddr));
    }
    str_cli(stdin, sockfd[0]);
    return 0;
}
当客户终止时,所有打开的描述符由内核自动关闭(不close,仅exit),5个连接在同一时刻终止;这就引发了5个FIN,每个连接一
个,它们反过来使服务器的5个子进程基本在同--时刻终止,这又导致差不多在同一时刻有5个SIGCHLD信号递交给父进程,如下所示

学新通
学新通
学新通

本次测试服务端,仍使用上述服务端代码,其中使用wait进行处理;
但在本次测试中,可知wait并不足以防止出现僵尸进程;
【原因】:所有5个信号都在信号处理函数之前产生,而该函数值处理一次;
【解决】:故我们应该使用wiatpid函数,我们将该函数置于一个循环中,以获取所有已终止子进程状态;
	且必须指定WNOHANG选项,告知waitpid在尚未终止的子进程在运行时不要阻塞;
【为什么wait不可以呢?】:不能在循环内调用wait,由于没有办法防止该函数在正运行的子进程尚有未终止时阻塞;
void sig_pid_chld(int signo) {
    pid_t pid;
    int stat;
    while ((pid = waitpid(-1, &stat, WNOHANG)) > 0){
        printf("child %d terminited\n", pid);
    }
    return ;
}

学新通

上述中演示了在网络编程中可能遇到的错误:
- 当fork子进程时,必须捕捉SIGCHLD信号;
- 当捕捉信号时,必须处理被中断的系统调用;
- SIGCHLD的信号处理函数必须正确编写,应使用waitpid函数以免留下僵尸进程;

完整服务端代码

#include "../Jxiepc/unp.h"
#include <iostream>

using namespace std;


int main() {
    int lfd, cfd;

    pid_t pid;
    char buf[128];
    socklen_t clen;
    struct sockaddr_in s_addr, c_addr;

    lfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&s_addr, sizeof(s_addr));
    s_addr.sin_family = AF_INET;
    s_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    s_addr.sin_port = htons(SERV_PORT);

    bind(lfd, (struct sockaddr*)&s_addr, sizeof(s_addr));

    listen(lfd, LISTENQ);
    Signal(SIGCHLD, sig_pid_chld);

    while (1) {
        clen = sizeof(c_addr);
        cfd = accept(lfd, (struct sockaddr *) &c_addr, &clen);
        cout << "connect from " <<
             inet_ntop(AF_INET, &c_addr.sin_addr, buf, sizeof(buf))
             << ",  port " << ntohs(c_addr.sin_port) << endl;
        if ((pid = fork()) == 0) {
            close(lfd);
            str_echo(cfd);
            exit(0);
            close(cfd);
        }
    }
    return 0;
}
学新通

8、accept返回前连接终止

除了系统中断,以下将讲述另外中accpet返回错误,只需重新调用accept即可;
【问题描述】:
	三路握手后,客户TCP发送了RST,在服务器看来该连接已由TCP排队,等待服务器进程调用accept的RST到达;稍后,服务器进程调用accept;
【如何解决】:在次调用accept即可;
【模拟该问题】:在调用accept前先sleep一段时间,启动客户,后即会发送该情况;

学新通

9、服务器进程终止

【模拟进程终止步骤】:
- 在同一个主机上启动c和s,在客户上键入文本,以验证正常;
- 使用kill杀死服务器子进程的进程ID,作为进程终止处理的部分工作,子进程中所有打开着的描述符都被关闭;
	这就导致向客户发送一个FIN,而客户TCP则响应以一个ACK;这就是TCP连接终止工作的前半部分;
- SIGCHLD信号被发送给服务器父进程,并得到正确处理;
- 客户TCP接收来自服务器TCP的FIN并响应以一个ACK,然后是客户进程阻塞在fgets调用上,等待从终端接收一行文本;

学新通

学新通

- FIN接收并没有告知客户TCP服务器进程已终止,故响应以一个RST;
	由于客户在调用write后立即调用readline,故看不到该RST;
- 当客户终止时,所有打开着的描述符都被关闭;
【问题】:当FIN到达套接字时,客户正在阻塞在fgets上;

10、SIGPIPE信号

当客户不处理readline返回的错误,反而写入更多的数据到服务器上,将会发送什么呢?
	- 客户可能在读回任何数据之前执行两次针对服务器的写操作,而RST是由其中第一次写操作引发的;
- 当一个进程向某个已收到的RST的套接字执行写操作时,内核向该进程发送一个SIGPIPE信号,该信号默认行为时终止进程,故
进程必须捕获它以免不情愿地被终止;
- 若有错误,写操作都将返回EPIPE错误; 
/**
以下使用两次write为了第一个使用write以引发RST、第二个write产生SIGPIPE;
*/
void str_cli01(FILE *fp, int sockfd) {
    char sendline[MAXLINE], recvline[MAXLINE];
    while (fgets(sendline, MAXLINE, fp) != NULL) {
        write(sockfd, sendline, 1);
        sleep(1);
        write(sockfd, sendline 1, strlen(sendline)-1);
        if(Readline(sockfd, recvline, MAXLINE) == 0)
            err_quit("str_cli: server terminated prematurely");
        fputs(recvline, stdout);
    }
}
处理SIGPIPE的建议方法取决于它发生时应用进程想做什么;若无事情要做,则信号设为SIG_IGN,捕捉EPIPE错误并终止;

若信号出现时需采取特殊措施,则必须捕获,在信号处理函数中执行操作;但需知,若使用了多个套接字,无法确定是哪个套接
字的;若需要知道哪个,则必须要么不理会该信号,要么从信号处理函数返回后再处理来自write的EPIPE;

11、服务器主机崩溃

- 当服务器崩溃时,已有的网络连接上不发出任何东西;
- 客户上键入一行文本,它由writen写入内核,再由客户TCP作为一个数据分节送出,客户随后阻塞于readline调用,等待回射的应答;
- 若用tcpdump观察网络,可知客户TCP持续重传数据分节,试图从服务器上接收一个ACK;重传后也无果,当客户TCP最后放弃
	时,由于阻塞在readline上,将返回一个错误ETIMEDOUT(服务器奔溃);
	然而如果某个中间路由器判定服务器主机已不可达,从而响应以一个(目的地不可达)ICMP消息,那么所返回的错误是				
	EHOSTUNREACH或ENETUNREACH;
- 最终会发现对端主机已崩溃或不可达,但需要等待9分钟检测,故对readline调用设置一个超时;

12、服务器主机崩溃后重启

【模拟】:先建立连接,在从网络上断开服务器主机,将它关机后重启,在重新连接到网络;
【步骤】:
	- 我们启动服务器和客户,并在客户键入一行文本以确认连接已经建立;
	- 服务器主机崩溃并重启;
	- 在客户上键入一行文本,它将作为一个TCP数据分节发送到服务器主机。
	- 当服务器主机崩溃后重启时,它的TCP丢失了崩溃前的所有连接信息,因此服务器TCP对于所收到的来自客户的数据分节响应以一个RST。
	- 当客户TCP收到该RST时,客户正阻塞于readline调用,导致该调用返回ECONNRESET;
		如果对客户而言检测服务器主机崩溃与否很重要,即使客户不主动发送数据也要能检测出来,即需要使用其他技术;

13、服务器主机关机

- 关机时,init进程通常先给所有进程发送SIGTERM信号,等待一段固定的时间(5到20秒),后给所有仍运行的进程发送SIGKILL信号(不能被捕获)。为了清除和终止进程;
- 如果我们不捕获SIGTERM信号并终止,我们的服务器将由SLGKILL信号终止;
- 当服务器子进程终止时,它的所有打开着的描述符都被关闭,我们必须在客户中使用select或poll函数,使得服务器进程的终止一经发生,客户就能检测到;

14、TCP例子

C/S通信前,每一端都指定连接的套接字对:本地IP、端口、外地IP、端口;
- 外地IP、端口必须在客户调用connect时指定;
- 而两个本地值通常就由内核作为connect的一部分来选定;

学新通
学新通

- 本地端口由bind指定,bind调用中服务器指定的本地IP地址通常是通配IP地址;
- 若服务器在一个多宿主机上绑定通配IP地址,则可在连接后通过调用getsockname来确定本地IP地址;
- 两个外地值则由accept调用返回给服务器;
- 如果另外一个程序由调用accept的服务器通过调用exec来执行,那么这个新程序可以在必要时调用getpeername来确定客户的IP
	地址和端口号;

15、数据格式

上述例子中,没有对服务器接收的数据格式限定要求;

15.1 传递文本串

void str_echo08(int sockfd) {
    long arg1, arg2;

    ssize_t n;
    char line[MAXLINE];

    while (1) {
        if ((n == readline(sockfd, line, MAXLINE)) == 0)
            return;
        if (sscanf(line, "%ld%ld", &arg1, &arg2) == 2)
            snprintf(line, sizeof(line), "%ld\n", arg1   arg2);
        else
            snprintf(line, sizeof(line), "input error\n");
        n = strlen(line);
        writen(sockfd, line, n);
    }
}
学新通

15.2 传递二进制结构

struct args {
	long arg1;
	long arg2;
};

struct result {
	long num;
};

void str_echo09(int sockfd) {
    struct args args;
    struct result result;
    ssize_t n;

    while (1) {
        if((n = Readn(sockfd, &args, sizeof(args))) == 0) {
            return;
        }
        result.sum = args.arg1   args.arg2;
        writen(sockfd, &result, sizeof(result));
    }
}

int str_cli09(FILE *fp, int sockfd) {
    char senline[MAXLINE];
    struct args args;
    struct result result;
    while (fgets(senline, MAXLINE, fp) != NULL) {
        if (sscanf(senline, "%ld%ld", &args.arg1, &args.arg2) != 2) {
            printf("invalid input: %s", senline);
            continue;
        }
        writen(sockfd, &args, sizeof(args));
        if (Readn(sockfd, &result, sizeof(result)) == 0)
            err_sys("str_cli: server terminated prematurely");
        printf("%\d\n", result.sum);
    }
}
学新通

上述例子中存在的问题

- 注意存储二进制数(大端与小端);
- 不同的实现在存储相同的C数据类型上可能存在差异;如,大多数32位Unix系统使用32位表示长整数,而64位系统却典型地使用
	64位来表示同样的数据类型。对于short、int或long等整数类型,它们各自的大小没有确定的保证;
- 不同的实现给结构打包的方式存在差异,取决于各种数据类型所用的位数以及机器的对齐限制;一般不传送二进制结构;

解决

- 把所有的数值数据作为文本串来传递;
- 显式定义所支持数据类型的二进制格式(位数、大端或小端字节序),并以这样的格式在客户与服务器之间传递所有数据;

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfkbjjk
系列文章
更多 icon
同类精品
更多 icon
继续加载