Unix 网络编程(四)- 典型TCP客服服务器程序开发实例及基本套接字API介绍

转载:http://blog.csdn.net/michael_kong_nju/article/details/43457393

写在开头:

在上一节中我们学习了一些基础的用来支持网络编程的API,包括“套接字的地址结构”、“字节排序函数”等。这些API几乎是所有的网络编程中都会使用的一些,对于我们正确的编写网络程序有很大的作用。在本节中我们会介绍编写一个基于TCP的套接字程序需要的一些API,同时会介绍一个完整的TCP客户服务器程序,虽然这个程序功能相对简单,但确包含了一个客户服务器程序所有的步骤,一些复杂的程序也都是在此基础上进行扩充。在后面随着学习的深入,我们会给这个程序添加功能。

下面我们首先给出这个程序实例,然后根据程序分析其中用到的套接字函数,这些套接字函数也是其他的TCP网络编程中都会使用到的,包括像:socket 函数,connect 函数,bind 函数,listen 函数,accept函数,fork和exec函数等,其实在之前(一)中已经使用了,而且也有了部分的介绍,这里将会给出详细的说明  。

------------------------------------------------------------------------------------------------------------------------------

TCP客户服务器程序

我们这里的服务器程序是一个回射服务器,实现以下功能:

(1) 客户从标准输入中读入一行文本,然后将文本写给服务器;

(2) 服务器从网络输入读入这行文本,并回射给用户;

(3) 客户从网络输入中读入这行文本,并显示在标准输出中。 

功能的模型如下面所示:


下面是具体的服务器端和客户端的程序,可以在我们所下载的源码tcpcliserv/tcpcli01.c 和tcpcliserv/tcpserv01.c中找到,但是为了让大家更直观的看到最原始的样子,这里重写

了Richard老先生的代码,你可以直接拷贝,并用gcc编译然后在你机器上运行。

下面是服务器端代码  echo_server.c

通过创建子进程来处理客户端的请求从而实现服务器的并发。服务器会调用下面的str_echo的函数,他将客户端发送过来的内容按原样返回。

[cpp] view plaincopy
print?
  1. #include <stdio.h>  
  2. #include <sys/types.h>  
  3. #include <sys/socket.h>  
  4. #include <netinet/in.h>  
  5. #include <signal.h>  
  6. #include <errno.h>  
  7. #define LISTENQ 5  
  8.   
  9. #define MAXLINE 2048  
  10. #define SA  struct sockadddr  
  11. #define SERV_PORT 9877  
  12.   
  13. void str_echo(int sockfd);  
  14. int  
  15. main(int argc, char **argv)  
  16. {  
  17.     int                 listenfd, connfd;  
  18.     pid_t               childpid;  
  19.     socklen_t           clilen;  
  20.     struct sockaddr_in  cliaddr, servaddr;  
  21.     listenfd = socket(AF_INET, SOCK_STREAM, 0);   
  22.     bzero(&servaddr, sizeof(servaddr));  
  23.     servaddr.sin_family      = AF_INET;  
  24.     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  
  25.     servaddr.sin_port        = htons(SERV_PORT);  
  26.   
  27.     bind(listenfd, (SA *) &servaddr, sizeof(servaddr));  
  28.   
  29.     listen(listenfd, LISTENQ);  
  30.   
  31.   
  32.     for ( ; ; ) {   
  33.         clilen = sizeof(cliaddr);  
  34.         //connfd = accept(listenfd, (SA *) &cliaddr, &clilen);  
  35.         connfd = accept(listenfd,(SA*)NULL, NULL);  
  36.         printf("Successfully Connected!\n");  
  37.         if ( (childpid = fork()) == 0) {    /* child process */  
  38.             close(listenfd);    /* close listening socket */  
  39.             str_echo(connfd);   /* process the request */  
  40.             exit(0);  
  41.         }     
  42.         close(connfd);          /* parent closes connected socket */  
  43.     }     
  44. }  
  45. void  
  46. str_echo(int sockfd)  
  47. {  
  48.     ssize_t     n;  
  49.     char        buf[MAXLINE];  
  50.   
  51. again:  
  52.     while ( (n = read(sockfd, buf, MAXLINE)) > 0)  
  53.     {  
  54.             printf("write back to the client!\n");  
  55.             write(sockfd, buf, n);  
  56.             //printf("write back to the client!");  
  57.     }  
  58.     if (n < 0 && errno == EINTR)  
  59.         goto again;  
  60.     else if (n < 0)  
  61.     {  
  62.         perror("str_echo: read error");  
  63.         exit(1);  
  64.     }  
  65. }    

下面是客户端的程序,echo_tcp_client.c

它发起和服务器连接,然后从标准输入中读入数据然后通过socket发送给服务器,并读取从socket回射的程序。

[cpp] view plaincopy
print?
  1. #include <stdio.h>  
  2. #include <sys/types.h>  
  3. #include <sys/socket.h>  
  4. #include <netinet/in.h>  
  5.   
  6. #define LISTENQ 5  
  7. #define MAXLINE 2048  
  8. #define SERV_PORT 9877  
  9.   
  10. typedef struct sockaddr SA;   
  11.   
  12. void str_cli(FILE *fp, int sockfd);  
  13. int  
  14. main(int argc, char **argv)  
  15. {  
  16.     int                 sockfd;  
  17.     struct sockaddr_in  servaddr;  
  18.   
  19.   
  20.     if (argc != 2)  
  21.     {     
  22.         perror("usage: tcpcli <IPaddress>");  
  23.         exit(-1);  
  24.     }     
  25.     sockfd = socket(AF_INET, SOCK_STREAM, 0);   
  26.   
  27.   
  28.     bzero(&servaddr, sizeof(servaddr));  
  29.     servaddr.sin_family = AF_INET;  
  30.     servaddr.sin_port = htons(SERV_PORT);  
  31.     inet_pton(AF_INET, argv[1], &servaddr.sin_addr);  
  32.     if(connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0)  
  33.     {     
  34.         perror("Connect Error!");  
  35.         exit(1);  
  36.     }     
  37.     else  
  38.         printf("Connected Successfully!\n");  
  39.     str_cli(stdin, sockfd);     /* do it all */  
  40.     exit(0);  
  41. }  
  42. str_cli(FILE *fp, int sockfd)  
  43. {  
  44.     char    sendline[MAXLINE], recvline[MAXLINE];  
  45.     while (fgets(sendline, MAXLINE, fp) != NULL) {  
  46.         write(sockfd, sendline, strlen(sendline));  
  47.         if (read(sockfd, recvline, MAXLINE) == 0)  
  48.         {  
  49.             perror("str_cli: server terminated prematurely");  
  50.             exit(-1);  
  51.         }  
  52.         fputs(recvline, stdout);  
  53.     }  
  54. }    

编译两个程序:

[sql] view plaincopy
print?
  1. gcc -O2 - wall echo_tcp_server.c -o tcpsvr01  
  2. gcc -O2 -wall echo_tcp_client.c -o tcpcli01  

然后在两个进程中分别将服务器和客户端运行起来,如下所示在客户端可以看到我们输入一行之后按回车会显示相同的从服务器端传回来的文本。


--------------------------------------------------------------------------------------------------------------------------

至此这个程序运行起来了,下面我们开始介绍服务器程序和客户端程序中的套接字函数是怎样将整个功能完成的,这里的函数包括:socket 函数,connect 函数,bind 函数,listen 函数,accept函数,fork和exec函数 等。首先我们给出整个函数被调用的一个流程图,这个流程图是根据tcp协议建立起来的:


这就是整个函数被调用过程的一个流程以及完成的功能。下面我们详细的介绍这些函数的用法:

socket 函数

socket 函数是进程执行网络I/O操作第一件需要做的事情,通过调用socket 函数来指定期望的通信协议类型并返回一个套接字描述符用来标识这个连接,套接字描述符,简称sockfd,是一个小的非负整数值类似于文件描述符。用法:

[cpp] view plaincopy
print?
  1. #include <sys/socket.h>  
  2. int socket ( int family, int type, int procotol);   /* 返回:若创建成功返回一个非负sockfd, 否则返回 -1 */  

例如上面服务器端程序中的

 listenfd = socket(AF_INET, SOCK_STREAM, 0);  

family: 代表的是协议族,指明该套接字在网络层使用什么来输出,包括:AF_INET(IPv4), AF_INET6(IPv6),  AF_LOCAL(Unix 域协议), AF_ROUTE(路由套接字), AF_KEY(秘钥套接字)

type: 指明套接字使用的数据流的类型,包括 SOCK_STREAM(字节流套接字), SOCK_DGRAM (数据报套接字),SOCK_SEQPACKET(有序分组套接字), SOCK_RAW(原始套接字)等;

protocol:  指明的套接字使用的传输层协议类型,包括:IPPROTO_TCP(TCP传输协议) IPPROTO_UDP(UDP传输协议),IPPROTO_SCTP(SCTP传输协议等);一般为了省事直接将这个字段置0,由给定的family和type来决定使用什么协议。

connect函数

connect 函数是客户端用来和服务器建立连接使用的。调用connect 函数,会引起TCP三次握手的建立。下面是具体的API

[cpp] view plaincopy
print?
  1. #include <sys/socket.h>  
  2. int connect ( int sockfd, const struct sockaddr *servaddr, socklen_t addrlen); /* 成功返回0,出错返回-1*/  

socketfd 就是socket 函数返回的那个套接字描述符用来表示这个连接;

*servaddr 是一个指向y要连接的服务器的套接字地址结构的指针,这里需要强制类型转换成通用地址结构,在上一节(三)中我们讲过这个套接字地址结构的几个类型;

addrlen 是这个地址的大小;

如上面事例中

connect(sockfd, (SA *) &servaddr, sizeof(servaddr));

connect被调用时大概会发生如下几种情况:

(1) 如果目标地址可达,并运行了服务器程序,那么就正常返回,不会出错;

(2) 如果目标地址可达,但是没有运行服务器程序,那么出错返回: connect error: Connection Refused;

(3) 如果目标地址在同一个网络,但是不可达,那么出错返回: connect error: connection timed out;(大概是75s之后返回这个错误)

(4) 如果目标地址不在同一个网络,而且无法路由,那么直接返回: connect error: No route to host.

bind函数

bind 函数是给一个socket 绑定一个套接字地址结构(或者更准确的是:将一个本地协议地址赋予一个套接字),在这个套接字地址结构中有使用的协议、ip、端口号等。如上面程序中:

  1.     servaddr.sin_family      = AF_INET;  
  2.     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  
  3.     servaddr.sin_port        = htons(SERV_PORT);  
  4.   
  5.     bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); 

这里是服务器调用Bind函数进行绑定,客户端也可以调用bind函数,但是不是很必要。它的API是:

[cpp] view plaincopy
print?
  1. #include <sys/socket.h>  
  2. int bind ( int sockfd, const struct sockaddr *myaddr, socklen_t addrlen); /* 成功返回0,出错返回-1*/  

对于客户端,调用这个函数是告诉服务器原地址和端口号是什么;

对于服务器,调用这个函数表名自己只会接受以这个ip地址和端口号为目的的客户端请求。

不过,他们都可以将这个地址设为通配地址(INADDR_ANY)端口号设为(0),这样就可以接受所有客户端的请求并且允许内核选择源ip地址和分配临时端口。

listen函数

listen 函数是服务器端调用的函数。从宏观上讲,listen发生在服务器端socket, bind函数之后,accept函数之前,调用它表明服务器端在监听来自客户端的请求。从细节的角度来讲,listen函数被调用之后,服务器端开始维护两个队列:一个队列称为未完成队列是刚监听到用户发起的连接请求组成的队列(接收到SYN),即三次握手的第一阶段,这个时候将这个socket 请求放在这个队列中;另一个队列称为已完成队列,是从未完成队列中将完成三次握手的socket调入的。具体的API是:

[cpp] view plaincopy
print?
  1. #include <sys/socket.h>  
  2. int listen( int sockfd, int backlog);  /* 成功返回0,出错返回-1*/  

这里的 backlog 没有确切的解释,通常认为是这两个队列中条目之和。但是这个值一般都会乘上一个模糊因子这里是1.5来规定最大。历史上这个值一般是5,现在因为服务器繁忙会取一个比较大的值;

accept 函数

accept 函数可以紧接着上面的listen函数讨论,accpet 函数被调用时将会从listen状态中的已完成连接套接字队列中选择队首进行服务。如果队列为空,那么将阻塞。下面是它的API:

[cpp] view plaincopy
print?
  1. #include <sys/socket.h>  
  2. int accept ( int sockfd,  struct sockaddr *cliaddr, socklen_t *addrlen); /* 成功返回非负套接字描述符,出错返回-1*/  

如上面服务器端程序所示:

 connfd = accept(listenfd, (SA *) &cliaddr, &clilen);  

其中:sockfd 是监听套接字的描述符, 第二个参数是这次连接的对端的协议地址,第三个参数是长度,这里是引用的形式,因为要往里面写数据。如果没必要得到这个地址,可以直接置0.

注意这个函数的返回值称为 连接套接字描述符, 和socket 返回的一次服务器进程中只创建一次的监听套接字描述符不同,这里每次调用accept 函数都会返回这么一个连接套接字描述符,然后对此描述符进行处理。

并发服务器 fork/exec函数

(一)中的服务器程序是一个简单的迭代的服务器程序,当accept一个客户请求之后服务器便一直为这个程序服务,对于这种简单的获取时间的程序来讲是可以的,但是有些服务器程序执行的操作需要花费很长时间,而且我们又不希望服务器一直在处理这么一个客户请求,所以就希望编写并发的服务器程序,使得服务器同时可以处理多个请求,而编写并发服务器最简单的方法就是fork 一个子进程来服务每个客户,我们上面的程序也是采用这种方式:

  1.         if ( (childpid = fork()) == 0) {    /* child process */  
  2.             close(listenfd);    /* close listening socket */  
  3.             str_echo(connfd);   /* process the request */  
  4.             exit(0);  
  5.         }  
  6.         close(connfd);          /* parent closes connected socket */  
01行是调用fork()函数来创建一个子进程,并由子进程来处理客户端的请求。这里的if语句中是fork()返回值为0的,表示是在子进程中处理,因为没必要listenfd所以直接关闭,Line3进行处理,line4关闭子进程。line6是父进程中关闭连接套接字描述符,因为他将这个请求交由子进程来处理,所以自己去accept新的请求。

下面是fork()函数的具体用法:

[cpp] view plaincopy
print?
  1. #include <unistd.h>  
  2. pid_t fork(void); /*在子进程中返回值是0;在父进程中返回值是子进程的id;若出错则返回-1*/  

这个函数也是我们迄今为止见过的为数不多的两个有两个返回值的函数,因为子进程调用getppid()函数可以获得父进程的id,所以在子进程中其返回值就直接是0了,而父进程因为要管理所有的子进程,所以就在父进程的返回值中拿到这个值;

注意,父进程和子进程共享在创建这个子进程之前的所有描述符。所以这里的connfd才可以在子进程中被引用,而且描述符的引用数会将1,所以当父进程close(connfd)的时候只会减1,只有子进程也close才会减为0;

getsockname 和 getpeername 函数

在一开始的事例程序中并没有这两个函数的影子,但是在后面的程序中,可能会用到这两个函数,所以这里有必要说明一下。

[cpp] view plaincopy
print?
  1. #include <sys/socket.h>  
  2. int getsockname (int sockfd, struct sockadddr * localaddr, socklen_t *addrlen);  
  3. int getpeername (int sockfd, struct sockadddr * peeraddr, socklen_t *addrlen);    /* 成功返回0,出错返回-1*/  

getsockname ()用来返回与sockfd这个套接字关联的本地协议组地址,通过这个地址可以查看内核赋予的ip地址和端口号,一般用于客户端不适用Bind函数而直接调用socket从而由内核决定本地ip地址和端口号是什么,这个时候用这个函数查看很有用;

getpeername()一般用于服务器在fork一个子进程处理一个客户端的请求时,而子进程内存映像因为被执行的具体程序覆盖而丢失了客户的协议地址,这个时候通过调用getpeername()函数可以重新获得。

我们将在后面碰到这两个函数的的程序中再讨论这两个函数。

总结:

我们在篇博文中首先给出了一个并发的典型的TCP客户服务程序,并运行了这个程序。之后我们从TCP协议的角度给出了每一个函数完成的功能,最后详细的分析这些函数的API。所有的客户和服务器程序都从socket开始,它返回一个套接字描述符,对于服务器而言返回的是监听套接字描述符。客户之后调用connect进行连接,内核发送三次握手,之后服务器调用bind, listen, 和accept函数等。accept之后就开始处理一个请求,这里讲解了通过调用fork()函数创建子进程,由子进程并发的调度。

2015/02/03  于南京 CSDN 如需转载请注明地址谢谢:http://blog.csdn.net/michael_kong_nju/article/details/43457393



本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/384292.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C库函数与系统函数的关系

转载于:https://www.cnblogs.com/lr1402585172/p/10464933.html

Unix网络编程(六)高级I/O技术之复用技术 select

转载&#xff1a;http://blog.csdn.net/michael_kong_nju/article/details/44887411 I/O复用技术 本文将讨论网络编程中的高级I/O复用技术&#xff0c;将从下面几个方面进行展开&#xff1a; a. 什么是复用技术呢&#xff1f; b. 什么情况下需要使用复用技术呢&#xff1f; c. …

Ubuntu在vmware虚拟机无法上网的解决方法

http://blog.csdn.net/xueyushenzhou/article/details/50460183 在vmware中安装Ubuntu之后&#xff0c;我们希望基本的功能如上网、传输文件等功能都是可用的&#xff0c;但是经常遇到不能上网的情况。使用笔记本时&#xff0c;我们经常希望能通过无线网卡上网&#xff0c;但是…

IO 多路复用之poll总结

http://www.cnblogs.com/Anker/p/3261006.html IO多路复用之poll总结 1、基本知识 poll的机制与select类似&#xff0c;与select在本质上没有多大差别&#xff0c;管理多个描述符也是进行轮询&#xff0c;根据描述符的状态进行处理&#xff0c;但是poll没有最大文件描述符数量的…

C++项目中的extern C {}

http://www.cnblogs.com/skynet/archive/2010/07/10/1774964.html 引言 在用C的项目源码中&#xff0c;经常会不可避免的会看到下面的代码&#xff1a; 123456789#ifdef __cplusplusextern "C" {#endif/*...*/#ifdef __cplusplus}#endif它到底有什么用呢&#xff0c;…

C语言实现单链表(带头结点)的基本操作(创建,头插法,尾插法,删除结点,打印链表)

http://blog.csdn.net/xiaofeige567/article/details/27484137 C语言实现单链表&#xff08;带头结点&#xff09;的基本操作&#xff08;创建&#xff0c;头插法&#xff0c;尾插法&#xff0c;删除结点&#xff0c;打印链表&#xff09; [plain] view plaincopy #include<…

单向循环链表C语言实现

http://blog.csdn.net/morixinguan/article/details/51771633 我们都知道&#xff0c;单向链表最后指向为NULL&#xff0c;也就是为空&#xff0c;那单向循环链表就是不指向为NULL了&#xff0c;指向头节点&#xff0c;所以下面这个程序运行结果就是&#xff0c;你将会看到遍历…

web服务器原理

什么是web服务器&#xff1f; 在Mosaic浏览器&#xff08;通常被认为是第一个图形化的web浏览器&#xff09;和超链接内容的初期&#xff0c;演变出了“web服务器”的新概念&#xff0c;它通过HTTP协议来提供静态页面内容和图片服务。在那个时候&#xff0c;大多数内容都是静态…

(C语言版)链表(三)——实现双向链表创建、删除、插入、释放内存等简单操作

http://blog.csdn.net/fisherwan/article/details/19760681 上午写了下单向循环链表的程序&#xff0c;今天下午我把双向链表的程序写完了。其实双向链表和单向链表也是有很多相似的地方的&#xff0c;听名字可以猜到&#xff0c;每个节点都包含两个指针&#xff0c;一个指针指…

(C++版)链表(一)——实现单向链表创建、插入、删除等相关操作

http://blog.csdn.net/fisherwan/article/details/25557545 前段时间用C语言实现了链表的相关操作&#xff0c;但是发现当时挺清楚的&#xff0c;过了一段时间又忘的差不多了&#xff0c;所以现在打算用C再实现一遍&#xff0c;由于初次用C实现&#xff0c;存在错误的地方还望大…

(C语言版)链表(二)——实现单向循环链表创建、插入、删除、释放内存等简单操作

http://blog.csdn.net/fisherwan/article/details/19754585 昨天写了单向链表的代码&#xff0c;今天上午把单向循环链表的程序给敲完了。链表的相关操作一样的&#xff0c;包含链表的创建、判断链表是否为空、计算链表长度、向链表中插入节点、从链表中删除节点、删除整个链表…

计科院首页静态网页

一.HTML代码 <!DOCTYPE html><html><head><meta charset"UTF-8"><title>首页</title> </head><body><div id"page"> <div id"page_head"> <div id"logo" aligncenter…

(C语言版)链表(四)——实现双向循环链表创建、插入、删除、释放内存等简单操作

http://blog.csdn.net/fisherwan/article/details/19801993 双向循环链表是基于双向链表的基础上实现的&#xff0c;和双向链表的操作差不多&#xff0c;唯一的区别就是它是个循环的链表&#xff0c;通过每个节点的两个指针把它们扣在一起组成一个环状。所以呢&#xff0c;每个…

(C语言版)链表(一)——实现单向链表创建、插入、删除等简单操作(包含个人理解说明及注释,新手跟着写代码)

http://blog.csdn.net/fisherwan/article/details/19701027 我学习了几天数据结构&#xff0c;今天下午自己写了一个单向链表的程序。我也是新手&#xff0c;所以刚开始学习数据结构的菜鸟们&#xff08;有大牛们能屈尊看一看&#xff0c;也是我的荣幸&#xff09;可以和我一起…

(C++版)链表(二)——实现单项循环链表创建、插入、删除等操作

http://blog.csdn.net/fisherwan/article/details/25561857 链表&#xff08;二&#xff09;单向循环链表的实现&#xff0c;下面实现代码&#xff1a; [cpp] view plaincopy <span style"font-size:18px;" deep"5">#include <iostream> #in…

(C++版)链表(三)——实现双向链表的创建、插入、删除等简单操作

http://blog.csdn.net/fisherwan/article/details/25649073 链表&#xff08;三&#xff09;实现双向链表操作&#xff0c;代码如下&#xff1a; [cpp] view plaincopy <span style"font-size:18px;" deep"5">#include <iostream> #include …

(C++版)链表(四)——实现双向循环链表创建、插入、删除等简单操作

http://blog.csdn.net/fisherwan/article/details/25649271 链表&#xff08;四&#xff09;实现双向循环链表简单操作&#xff0c;代码如下&#xff1a; [cpp] view plaincopy <span style"font-size:18px;" deep"5">#include <iostream> #…

双向链表的创建和相关操作

http://blog.csdn.net/jw903/article/details/38947753 双向链表其实是单链表的改进。 当我们对单链表进行操作时&#xff0c;有时你要对某个结点的直接前驱进行操作时&#xff0c;又必须从表头开始查找。这是由单链表结点的结构所限制的。因为单链表每个结点只有一个存储直接后…

登陆界面

界面展示&#xff1a; <!DOCTYPE html> <html><head><meta charset"utf-8"><title>电子邮件登录</title><link href"style.css" type"text/css" rel"stylesheet"></head><body>…

运用递归将两个链表进行连接

http://blog.csdn.net/zjut_ym/article/details/45008259 建立2个数据项按从大到小排列的链表&#xff0c;实现2个链表的合并&#xff0c;并输出合并后链表的数据项。 函数代码如下 #include<iostream> using namespace std; struct node{int data;node *next; }; node …