第5章-unix网络编程 TCP/服务端程序示例

这一章主要是完成一个完整的tcp客户/服务器程序.通过一很简单的例子.弄清客户和服务器如何启动,如何终止,发生了某些错误会发生什么.这些事很重要的

客户端代码

  1. #include "unp.h"
  2. //static void str_cli1(FILE*fp,int sockfd);
  3. int main(int argc,char *argv[])
  4. {
  5. int sockfd;
  6. struct sockaddr_in servaddr;
  7. sockfd=Socket(AF_INET,SOCK_STREAM,0);
  8. bzero(&servaddr,sizeof(servaddr));
  9. servaddr.sin_family=AF_INET;
  10. servaddr.sin_port=htons(SERV_PORT);
  11. Inet_pton(AF_INET,argv[1],&servaddr.sin_addr);
  12. Connect(sockfd,(SA*)&servaddr,sizeof(servaddr));
  13. str_cli(stdin,sockfd);//逻辑处理
  14. exit(0);
  15. }

str_cli函数代码

  1. #include "unp.h"
  2. void str_cli(FILE*fp,int sockfd)
  3. {
  4. char sendline[MAXLINE],recvline[MAXLINE];
  5. while(Fgets(sendline,MAXLINE,fp)!=NULL){//从键盘接收保存到sendline
  6. Writen(sockfd,sendline,strlen(sendline)); //写给服务端
  7. if(Readline(sockfd,recvline,MAXLINE)==0)//接收从服务端返回的数据
  8. err_quit("str_cli:服务端提前关闭");
  9. Fputs(recvline,stdout);//打印到屏幕
  10. }
  11. }

(1)三次握手状态

当客户端调用connect(阻塞状态)的时候.将发送一个syn.此时状态为syn_sent.

服务端此刻在监听.创建了一个syn未连接队列.一个已连接队列.接收到客户端的syn包,将放在syn未连接队列.调用accept(阻塞)将发送一个syn+ack.服务端状态为syn_recv

客户端接收到服务端的syn+ack后.connect返回.将发送一个ack给服务端.服务端接收到ack后.syn未连接将会被转移到已连接队列.然后Accept从中拿一个已连接socket返回...双方状态为ESTABLISH

此时客户端阻塞于str_cli函数中的fget调用.因为我们木有从键盘输入.

当服务端的accept返回.服务端调用fork.再有子进程调用str_echo.该函数调用了readline,readline调用了read.而read在等待客户数据的到来而阻塞

另外.服务端的父进程再次调用accept并且阻塞(未完成连接队列跟已完成的链接队列都为空).此时监听套接字为Listen状态

以上.我们将有3个进程都在睡眠.客户进程,服务器父进程,服务器子进程

  1. #include "unp.h"
  2. int main(int argc,char *argv[])
  3. {
  4. int listenfd=Socket(AF_INET,SOCK_STREAM,0);
  5. struct sockaddr_in servaddr;
  6. servaddr.sin_family=AF_INET;
  7. servaddr.sin_port=htons(SERV_PORT);
  8. servaddr.sin_addr.s_addr=0;
  9. Bind(listenfd,(SA*)&servaddr,sizeof(servaddr));
  10. Listen(listenfd,LISTENQ);
  11. for(;;){
  12. pid_t childpid;
  13. int connect=Accept(listenfd,NULL,NULL);
  14. if((childpid=Fork())==0){
  15. Close(listenfd);
  16. str_echo(connect);
  17. exit(0);
  18. }
  19. close(connect);
  20. }
  21. }

str_echo套接字回射数据

  1. #include "unp.h"
  2. void str_echo(int sockfd)
  3. {
  4. ssize_t n;
  5. char buf[MAXLINE];
  6. again:
  7. while((n=read(sockfd,buf,MAXLINE))>0)//读取
  8. Writen(sockfd,buf,n);//返回
  9. if(n<0&&errno==EINTR)
  10. goto again;
  11. else if(n<0)
  12. err_sys("str_echo :read error");
  13. }

客户端关闭状态

关闭客户端键入cltr+d使得客户端终止

过程分析

(1)我们键入eof.fgets返回一个空指针.str_cli返回

(2)str_cli返回到客户端main函数后.main通过exit终止

(3)进程一旦终止将关闭所有打开的描述符,内核将关闭客户打开的套接字.这将会使得客户端的tcp发送一个FIN(进入FIN_WAIT_1).

服务端TCP受到FIN.将返回ACK响应(服务端进入CLOSE_WAIT)状态.客户端收到服务端的ACK.将进入FIN_WAIT_2

(4)服务端受到由客户端发送的FIN.服务器子进程还阻塞到readline调用.接收了fin.将返回0.使得Str_echo函数返回到服务器子进程的main函数中

(5)服务器子进程通过调用exit来终止.这将导致所有描述符都关闭..子进程关闭已连接套接字回使得服务端发送FIN(LAST_ACK).

客户端一旦接收到服务端发来的FIN将进入time_wait状态.并发送一个ACK给服务端.服务端接收到ACK后.将处于close.

以上是tcp 4次断手的过程

(7)这里有一个bug.服务器子进程终止时.给父进程发送一个SIGCHLD信号.但以上代码没捕捉这个信号,默认忽略.

这样会使得子进程进入僵尸状态.占用系统资源.

POSIX信号处理

信号也称为软件中断.信号通常异步发生的

信号来源:一个进程发给其他进程,由内核发给某个进程.

信号发生对应的动作:

(1)我们可以提供一个函数,用来执行对应的动作,这样的函数表示信号处理函数,这种行为表示捕获信号.但有2个信号无法捕捉(SIGKILL跟SIGSTOP)

(2)把信号的处置设定为SIG_IGN来忽略它.

(3)把信号的处置设置为SIG_DFL启动默认处置(绝大多数是终止进程)

这是实现一个signal函数

  1. Sigfunc * signal(int signo,Sigfunc *func)
  2. {
  3. struct sigaction act,oact;
  4. act.sa_handler=func;
  5. sigemptyset(&act.sa_mask);// empty the block setmask
  6. act.sa_flags=0;
  7. if(sigaction(signo,&act,&oact)<0)
  8. return (SIG_ERR);
  9. return oact.sa_handler;
  10. }

1.当信号处理函数运行时,突然发生一个新的信号.这个信号是会被阻塞的,而且安装处理函数时在sa_mask信号集任何额外的信号也会被阻塞

2.一个信号被阻塞期间产生一次或多次,该信号被解除阻塞后,只会提交一次,可以用sigprocmask函数选择性的阻塞跟解除阻塞

处理僵尸进程

设置僵尸状态是为了维护子进程的信息,父进程可能在某个时刻需要获取子进程相关信息,这些信息包括子进程的进程ID,终止原因,资源利用信息,

如果父进程挂了,子进程处于僵尸状态.init 进程将成为这些子进程的父进程.一般会清理她们,去除僵尸状态.

用以下函数来捕捉信号

  1. void sig_chld(int signo)
  2. {
  3. pid_t pid;
  4. int stat;
  5. pid=wait(&stat);
  6. printf("child %d stoped\n",pid);
  7. }

(书上用的是solaris9特定的例子.以下是该环境说明.被信号打断的系统调用不会重启)

当子进程挂了.发送一个SIG_CHLD信号给父进程,此时父进程阻塞于accept,因为信号原因,accept被打断,将返回一个EInTR错误(被中断的系统调用),父进程不处理该错误,父进程将终止

我用的是centos.这个环境下,被信号打断的系统调用将会自动重启不会出现书上的错误

[[email protected] five]#

child 13099 stoped

处理被中断的系统调用

慢系统调用:表示可能永远阻塞的系统调用

适用于慢系统调用的基本规则:当阻塞于某个慢系统调用的一个进程捕获某个信号且处理函数返回时,该系统调用可能返回一个EINtR错误,有些内核会自动重启某些被中断的系统调用,不过为了方便移植,我们编写捕获信号函数时,必须被返回EINTR有准备,一般处理规则如下

  1. if((connfd=accept(listenfd,NULL,NULL)<0){
  2. if(errno==EINTR)
  3. continue;
  4. else
  5. err_sys("accept error");
  6. }

注意:这段代码表示的就是自己重启被中断的系统调用,不过有一个函数我们不能重启:connect.该函数返回EINTR.不能再次重启,否则立刻返回一个该套接字已被使用的错误,因为connect发送syn.一直没收到服务端的ack,一旦被打断,重启可能又发送syn.这样会又用该套接字会被认为是不对的,比如打断后,服务端恰好已发送了ack只是没接受到而已.

wait跟waitpid函数

  1. pid_t wait(int *statloc);
  2. pid_t waitpid(pid_t pid,int *statloc,int options);

服务器进程终止

服务器子进程杀死.将发送一个fin给客户端,客户端回一个ack给服务端.SIGCHLD信号.然而客户端此时依旧阻塞在fgets调用等待从中断接收一行文本

如果我们依旧客户端write发送数据给服务端,服务端接收数据时.发现之前打开那个套接字进程已经挂了,于是响应一个RST.

这里要分情况 1.如果客户端调用readline在收到RST包之前.那客户端readline返回0表示服务端已经关闭,否则如果客户端在收到RST后,才调用readline.那readline将会返回一个ECONNRESET(connection reset by peer)

这种情况一般发生在服务进程较客户进程提前终止。当服务进程终止时会向客户 TCP 发送 FIN 分节,客户 TCP
回应 ACK,服务 TCP 将转入 FIN_WAIT2 状态。此时如果客户进程没有处理该 FIN (如阻塞在其它调用上而没有关闭 Socket
时),则客户 TCP 将处于 CLOSE_WAIT 状态。当客户进程再次向 FIN_WAIT2 状态的服务 TCP 发送数据时,则服务 TCP
将立刻响应 RST。一般来说,这种情况还可以会引发另外的应用程序异常,客户进程在发送完数据后,往往会等待从网络IO接收数据,很典型的如
read 或 readline 调用,此时由于执行时序的原因,如果该调用发生在 RST 分节收到前执行的话,那么结果是客户进程会得到一个非预期的
EOF 错误。此时一般会输出“server terminated prematurely”-“服务器过早终止”错误

来源: http://lzy.iteye.com/blog/383884

这里的问题在于:当fin到达套接字时,客户阻塞在fgets.但客户实际在对应2个描述符--套接字跟用户输入,它也不能单纯阻塞到这个2个之中一个.而应该阻塞在其中任何一个.(select poll可以解决)

SIGPIPE信号

如果一个进程向已收到的RST的套接字继续执行写操作的话,内核将向该进程发送一个SIGPIPE信号,该信号默认行为是终止.进程必须捕获该信号以免不情愿的终止

服务器主机奔溃

(1)当服务器主机奔溃时,已有的网络连接发不出任何东西(不是正常关机)

(2)当客户write发送数据候,阻塞到readline中,等待回射的应答.

(3)用tcpdump观察,可以看出客户TCP一直在持续重传数据分节,想要重服务端接收一个Ack,一共等待9分钟放弃重传,一旦客户TCP放弃重传.将给客户端返回一个ETIMEDOUT错误..如果一个路由已经判断服务器主机不可达,将响应一个 "destination unreachable (目的不可达)ICMP消息",返回的错误将是 EHOSTUNREACH

服务器主机奔溃后重启

假设客户端已跟服务器建立连接,服务器突然断网挂了,然后又重启了(客户端不知道)

当客户端发送数据时,客户TCP将收到RST,客户正阻塞readline.这将导致readline返回ECONNRESET错误

服务器正常关机.

init进程通常先给所有进程发送SIGTERM信号(这样给所有运行的进程一小段时间来清除和终止).然后再给仍然在运行的进程发送SIGKILL,服务器进程终止过程跟之前一样,

来自为知笔记(Wiz)

时间: 2024-10-10 02:07:04

第5章-unix网络编程 TCP/服务端程序示例的相关文章

Java网络编程(TCP服务端)

1 package WebProgramingDemo; 2 3 import java.io.IOException; 4 import java.io.InputStream; 5 import java.net.ServerSocket; 6 import java.net.Socket; 7 8 public class ServerSocketDemo { 9 10 /** 11 * @param args 12 * @throws IOException 13 */ 14 publi

win32汇编实现一个简单的TCP服务端程序(WinSock的简单认知应用)

Windows网络编程,相信好多人都知道,但是我们一般都是用其他语言编写,例如C,C++,JAVA,python等等,这些语言都可以,但是汇编语言比较底层,利用它,我们可以更清晰的了解到网络编程的内在部分,这是其他语言不能相比的,好了,废话不多说,这其实就是这次的目的(毕竟水平欠缺,还是先来按照罗云斌老师的WIN32汇编书上的例子加以学习,举一反三吧). 说道网络编程,现在我所接触到的程序开发,工具软件的使用,库等等都是基于Windows平台的,想要了解Windows的网络编程就必须要知道Win

UNIX网络编程2.9端口号2.10端口号与TCP并发服务器

UNIX网络编程笔记(4)—TCP客户/服务器程序示例

TCP客户/服务器程序示例 这一章信息量开始大起来了,粗略来看它实现了简单的TCP客户/服务器程序,里面也有一些费解的细节. 1.概述 完整的TCP客户/服务器程序示例.这个简单的例子将执行如下步骤的一个回射服务器(这里的回射服务器就是服务简单的把客户端发送的消息返回给客户): 1)客户从标准输入读入一行文本,并写给服务器 2)服务器从网络输入读入这行文本,并回射给客户 3)客户从网络输入读入这行回射文本,并显示在标准输出上 这样实际上就构成了一个全双工的TCP连接. 本章就围绕了这个简单的TC

网络编程之TCP客户端开发和TCP服务端开发

开发 TCP 客户端程序开发步骤 创建客户端套接字对象 和服务端套接字建立连接 发送数据 接收数据 关闭客户端套接字 import socket if __name__ == '__main__': # 创建tcp客户端套接字 # 1. AF_INET:表示ipv4 # 2. SOCK_STREAM: tcp传输协议 tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 和服务端应用程序建立连接 tcp_c

unix网络编程第四章----基于TCP套接字编程

为了执行网络I/O操作.进程必须做的第一件事情就是调用Socket函数.指定期待的通信协议 #include<sys/socket.h> int socket(int family,int type,int protocol); family表示协议族,比如AF_INET,type表示套接字类型, protocol一般设置为0 family: AF_INET ipv4协议 type: SOCK_STREAM 字节流套接字 SOCK_DGRAM 数据报套接字 SOCK_RAW 原始套接字 pro

Unix网络编程学习笔记之第5章 TCP客户端/服务器程序示例

一. 一个简单TCP回射服务端程序 #include "unp.h" #define MAXLINE 1024 #define PORT 13 #define CONMAX 5 void err_sys(const char* s) { fprintf(stderr, "%s\n",s); exit(1); } void str_echo(int connfd) { int nbyte; char buff[MAXLINE+1]; again: while(nbyt

UNIX网络编程入门——TCP客户/服务器程序详解

前言 最近刚开始看APUE和UNP来学习socket套接字编程,因为网络这方面我还没接触过,要等到下学期才上计算机网络这门课,所以我就找了本教材啃了一两天,也算是入了个门. 至于APUE和UNP这两本书,书是好书,网上也说这书是给进入unix网络编程领域初学者的圣经,这个不可置否,但这个初学者,我认为指的是接受过完整计算机本科教育的研究生初学者,需要具有完整计算机系统,体系结构,网络基础知识.基础没打好就上来啃书反而会适得其反,不过对于我来说也没什么关系,因为基础课也都上得差不多了,而且如果书读

Unix网络编程之基本TCP套接字编程(上)

TCP客户/服务器实例 服务器程序 #include "unp.h" int main(int argc, char **argv) { int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; listenfd = Socket(AF_INET, SOCK_STREAM, 0); //1 bzero(&servaddr, sizeof(servad