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的函数,他将客户端发送过来的内容按原样返回。
- #include <stdio.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <signal.h>
- #include <errno.h>
- #define LISTENQ 5
- #define MAXLINE 2048
- #define SA struct sockadddr
- #define SERV_PORT 9877
- void str_echo(int sockfd);
- 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);
- bzero(&servaddr, sizeof(servaddr));
- servaddr.sin_family = AF_INET;
- servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
- servaddr.sin_port = htons(SERV_PORT);
- bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
- listen(listenfd, LISTENQ);
- for ( ; ; ) {
- clilen = sizeof(cliaddr);
- //connfd = accept(listenfd, (SA *) &cliaddr, &clilen);
- connfd = accept(listenfd,(SA*)NULL, NULL);
- printf("Successfully Connected!\n");
- if ( (childpid = fork()) == 0) { /* child process */
- close(listenfd); /* close listening socket */
- str_echo(connfd); /* process the request */
- exit(0);
- }
- close(connfd); /* parent closes connected socket */
- }
- }
- void
- str_echo(int sockfd)
- {
- ssize_t n;
- char buf[MAXLINE];
- again:
- while ( (n = read(sockfd, buf, MAXLINE)) > 0)
- {
- printf("write back to the client!\n");
- write(sockfd, buf, n);
- //printf("write back to the client!");
- }
- if (n < 0 && errno == EINTR)
- goto again;
- else if (n < 0)
- {
- perror("str_echo: read error");
- exit(1);
- }
- }
下面是客户端的程序,echo_tcp_client.c
它发起和服务器连接,然后从标准输入中读入数据然后通过socket发送给服务器,并读取从socket回射的程序。
- #include <stdio.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #define LISTENQ 5
- #define MAXLINE 2048
- #define SERV_PORT 9877
- typedef struct sockaddr SA;
- void str_cli(FILE *fp, int sockfd);
- int
- main(int argc, char **argv)
- {
- int sockfd;
- struct sockaddr_in servaddr;
- if (argc != 2)
- {
- perror("usage: tcpcli <IPaddress>");
- exit(-1);
- }
- sockfd = 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.sin_addr);
- if(connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0)
- {
- perror("Connect Error!");
- exit(1);
- }
- else
- printf("Connected Successfully!\n");
- str_cli(stdin, sockfd); /* do it all */
- exit(0);
- }
- str_cli(FILE *fp, int sockfd)
- {
- char sendline[MAXLINE], recvline[MAXLINE];
- while (fgets(sendline, MAXLINE, fp) != NULL) {
- write(sockfd, sendline, strlen(sendline));
- if (read(sockfd, recvline, MAXLINE) == 0)
- {
- perror("str_cli: server terminated prematurely");
- exit(-1);
- }
- fputs(recvline, stdout);
- }
- }
编译两个程序:
- gcc -O2 - wall echo_tcp_server.c -o tcpsvr01
- gcc -O2 -wall echo_tcp_client.c -o tcpcli01
然后在两个进程中分别将服务器和客户端运行起来,如下所示在客户端可以看到我们输入一行之后按回车会显示相同的从服务器端传回来的文本。
--------------------------------------------------------------------------------------------------------------------------
至此这个程序运行起来了,下面我们开始介绍服务器程序和客户端程序中的套接字函数是怎样将整个功能完成的,这里的函数包括:socket 函数,connect 函数,bind 函数,listen 函数,accept函数,fork和exec函数 等。首先我们给出整个函数被调用的一个流程图,这个流程图是根据tcp协议建立起来的:
这就是整个函数被调用过程的一个流程以及完成的功能。下面我们详细的介绍这些函数的用法:
socket 函数
socket 函数是进程执行网络I/O操作第一件需要做的事情,通过调用socket 函数来指定期望的通信协议类型并返回一个套接字描述符用来标识这个连接,套接字描述符,简称sockfd,是一个小的非负整数值类似于文件描述符。用法:
- #include <sys/socket.h>
- 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
- #include <sys/socket.h>
- 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、端口号等。如上面程序中:
- servaddr.sin_family = AF_INET;
- servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
- servaddr.sin_port = htons(SERV_PORT);
- bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
这里是服务器调用Bind函数进行绑定,客户端也可以调用bind函数,但是不是很必要。它的API是:
- #include <sys/socket.h>
- 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是:
- #include <sys/socket.h>
- int listen( int sockfd, int backlog); /* 成功返回0,出错返回-1*/
这里的 backlog 没有确切的解释,通常认为是这两个队列中条目之和。但是这个值一般都会乘上一个模糊因子这里是1.5来规定最大。历史上这个值一般是5,现在因为服务器繁忙会取一个比较大的值;
accept 函数
accept 函数可以紧接着上面的listen函数讨论,accpet 函数被调用时将会从listen状态中的已完成连接套接字队列中选择队首进行服务。如果队列为空,那么将阻塞。下面是它的API:
- #include <sys/socket.h>
- 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 一个子进程来服务每个客户,我们上面的程序也是采用这种方式:
- if ( (childpid = fork()) == 0) { /* child process */
- close(listenfd); /* close listening socket */
- str_echo(connfd); /* process the request */
- exit(0);
- }
- close(connfd); /* parent closes connected socket */
01行是调用fork()函数来创建一个子进程,并由子进程来处理客户端的请求。这里的if语句中是fork()返回值为0的,表示是在子进程中处理,因为没必要listenfd所以直接关闭,Line3进行处理,line4关闭子进程。line6是父进程中关闭连接套接字描述符,因为他将这个请求交由子进程来处理,所以自己去accept新的请求。
下面是fork()函数的具体用法:
- #include <unistd.h>
- pid_t fork(void); /*在子进程中返回值是0;在父进程中返回值是子进程的id;若出错则返回-1*/
这个函数也是我们迄今为止见过的为数不多的两个有两个返回值的函数,因为子进程调用getppid()函数可以获得父进程的id,所以在子进程中其返回值就直接是0了,而父进程因为要管理所有的子进程,所以就在父进程的返回值中拿到这个值;
注意,父进程和子进程共享在创建这个子进程之前的所有描述符。所以这里的connfd才可以在子进程中被引用,而且描述符的引用数会将1,所以当父进程close(connfd)的时候只会减1,只有子进程也close才会减为0;
getsockname 和 getpeername 函数
在一开始的事例程序中并没有这两个函数的影子,但是在后面的程序中,可能会用到这两个函数,所以这里有必要说明一下。
- #include <sys/socket.h>
- int getsockname (int sockfd, struct sockadddr * localaddr, socklen_t *addrlen);
- 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
Unix 网络编程(四)- 典型TCP客服服务器程序开发实例及基本套接字API介绍相关推荐
- 网络编程学习笔记(TCP回射服务器程序修订版)
服务器端: #include <sys/socket.h> #include <stdio.h> #include <netinet/in.h> #include ...
- 《Windows网络编程案例教程》-董相志 学习记录 阻塞/非阻塞套接字编程
<Windows网络编程案例教程>-董相志 学习记录 阻塞/非阻塞套接字编程 2.3 阻塞/非阻塞套接字编程 阻塞套接字编程通信流程图 2.3.1 阻塞套接字客户机编程 1.启动并初始化W ...
- UNIX网络编程笔记(1):TCP简介
1.简介 TCP(Transmission Control Protocol),即传输控制协议,是一种面向连接的.可靠的.基于字节流的传输层通信协议.TCP协议有以下几个特点: TCP提供客户与服务器 ...
- UNIX网络编程笔记(3):简单的并发服务器
上一讲中的简单时间获取服务器是一个迭代服务器,对于获取时间来说够用了.迭代服务器有这样的特点:同一时间只能给一个客户服务.也就是说,如果某一时刻服务器与某个客户正在连接,其它客户必须等到上一个客户与服 ...
- UNIX网络编程笔记(4):简单的回射程序
上一讲中我们通过调用fork函数实现了一个简单的并发时间获取服务器.这是一个简单的并发服务器框架,然而这里使用这个框架实现一个简单的回射服务器会出现一个问题,这个问题就是僵尸子进程. 1.回射程序 下 ...
- 网络编程:使用tcp协议实现服务器与客户端交互
服务器 ************************************************************************************************ ...
- java微信的客服接口开发,微信公众号 客服接口的开发实例详解
微信平台更新之后,发现客服接口不错.研究了下 和大家分享下. 按照官方文档,是向客服接口发送规定的JSon 就可以了. 首先先封装下 JSon 的类: package com.lwz.wx.bean. ...
- Unix 网络编程基础
本专题内容均来自 Stevens 先生的 Unix 网络编程 卷I TCP 建立与中止 三次握手 从图中可以看出: 服务端必须准备好接受外来的连接,称之为被动打开.调用的函数为 socket, bin ...
- 《UNIX网络编程 卷1:套接字联网API》学习笔记——基本TCP套接字编程
UNIX网络编程--基本TCP套接字编程 socket 函数 connect 函数 bind 函数 listen 函数 accept 函数 fork 和 exec 函数 并发服务器 close 函数 ...
最新文章
- Mine Sweeper II
- LeetCode 500. 键盘行
- CSS Repeater - 交错显示行背景色 table行鼠标进入事件特效 禁止文本换行
- 2019最新 Java商城秒杀系统的设计与实战视频教程(SpringBoot版)_1-4系统的整体演示...
- 由有理函数的广义积分引入,谈谈复变函数论中的留数
- UI设计中常见的各种布局有哪些?|优漫动游
- jxls中自定义函数的使用
- freemarker导出excel
- matlab做偏最小二乘回归
- android有什么作用,Android 7.0有什么功能 Android N完整功能参数介绍
- 如何在注册表里面删除所有qq相关文件
- Day25:Python基础编程(函数)能力训练50天——回文数
- clickhouse--物化视图
- obs windows 编译 obs browser
- 机床数控改造控制系统设计(微型计算机),大学生毕业论文:普通车床数控研究及改造设计...
- 「面试必背」Java集合面试题(收藏)
- 解决jquery版本过低引发的XSS跨站安全漏洞
- 关于java软文_2018年最好的微商护肤品朋友圈软文(文案)
- 一款c#实现的实用好玩儿的背单词程序
- C语言单链表基本操作,非常全面
热门文章
- python 打造一个sql注入脚本 (一)
- perl exe执行提示缺少文件解决方法
- 如何加快Json 序列化?有哪些方法?
- 利用 dbghelp.dll 生成 dump 文件
- java statement 返回类型,6.3 返回类型和返回语句 | Return type Return statement
- java代码编写的文本特征提取_Test1 java语言写的特征提取源代码,有搞文字识别的可以下载一看,简单易学 Develop 274万源代码下载- www.pudn.com...
- java比较时间sql_如何正确比较日期 java.sql.Date
- mysql5.7.17解压版安装_Windows中 MySQL5.7.17解压版安装步骤
- golang jwt设置过期_听说你的JWT库用起来特别扭,推荐这款贼好用的!
- html语言dl与ul,HTML中DL、UL、OL用哪个比较好