1. 其他函数准备
1. TCP 回射服务器程序: str_echo 函数
#include “unp.h”
void str_echo(int sockfd)
{
ssize_t n;
char buf[MAXLINE];
again:
/*
write()
函数定义:ssize_t write (int fd, const void * buf, size_t count);
函数说明:write()会把参数buf所指的内存写入count个字节到参数fd所指的文件内。
返回值:如果顺利write()会返回实际写入的字节数(len)。当有错误发生时则返回-1,错误代码存入errno中。
*/
/*
read()
函数定义:ssize_t read(int fd, void * buf, size_t count);
函数说明:read()会把参数fd所指的文件传送count 个字节到buf 指针所指的内存中。
返回值:返回值为实际读取到的字节数, 如果返回0, 表示已到达文件尾或是无可读取的数据。若参数count 为0, 则read()不会有作用并返回0。另外,以下情况返回值小于count:
*/
while((n = read(sockfd, buf, MAXLINE)) > 0)
Writen(sockfd, buf, n);
if(n < 0 && errno == EINTR) // 被中断后继续执行
goto again;
else if(n < 0)
err_sys("str_echo: read error");
}
2. TCP 回射客户端程序: str_cli 函数
#include "unp.h"
void str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE];
while(Fgets(sendline, NAXLINE, fp) != NULL)
{
Writen(sockfd, sendline, strlen(sendline));
if(Readline(sockfd, recvline, MALINE) == 0)
err_quit("str_cli: server terminated prematurely");
Fputs(recvline, stdout);
}
}
显示客户IP地址和端口号的时间获取服务器程序
#include "unp.h"
#include <time.h>
#pragma clang diagnostic push
#pragma ide diagnostic ignored "EndlessLoop" //这是防止IDE输出警告的,不用管。
int
main(int argc, char **argv)
{
int listenfd, connfd;//
//len是一个 值——结果 变量
socklen_t len;
struct sockaddr_in servaddr,cliaddr;//cliaddr存放客户的协议地址
//缓冲区存放转换结果
char buff[MAXLINE];
//存放时间
time_t ticks;
//初始化套接字,指定协议类型(第一个参数),套接字类型(字节流,数据报,或者原始套接字(极少使用))
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(13);
//本地协议地址赋予套接字
//参数一:套接字描述符,用于标识套接字
//参数二:指定特定于协议的地址结构的指针
//参数三:地址结构的长度
/*详细解释下为什么要传如地址结构的长度:由于该结构体的传递方向为从进程到内核,所以内核需要知道有多大的内容复制进来,所以需要一个参数告知内核传入地址的大小,这种变量叫做“值——结果”变量*/
Bind(listenfd,(SA*) &servaddr,sizeof (servaddr));
//使用socket创建的套接字最初为主动套接字,listen将其转换为一个被动套接字,指示内核应该接受指向该套接字的连接请求
Listen(listenfd,LISTENQ);
while (1)
{
len =sizeof (cliaddr);
//accept()函数功能是,从处于 established 状态的连接队列头部取出一个已经完成的连接,如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。
//如果accept成功,那么它的返回值是由内核生成的全新的描述符,代表与客户之间的 已成功建立 的TCP链接(已连接套接字描述符)
//参数一:监听套接字描述符
//参数二:客户进程的协议地址
//参数三:该地址的大小
//监听套接字与已连接套接字的区别:监听套接字在该服务的生命周期内一直存在,而每个已完成三次握手的TCP链接都会分配一个已连接套接字
connfd = Accept(listenfd,(SA*) &cliaddr,&len);
printf("connection from %s , port %d\n",
inet_ntop(AF_INET,&cliaddr.sin_addr,buff,sizeof (buff)),//打印IP地址
ntohs(cliaddr.sin_port));//打印端口号
//获取时间
ticks = time(NULL);
//为什么不使用printf:因为printf的缓冲区不可控制,对我们来说难以控制
snprintf(buff,sizeof (buff),"%.24s\r\n",ctime(&ticks));
Write(connfd,buff,strlen(buff));
Close(connfd);
}
}
#pragma clang diagnostic pop
4.2作业
#include "unp.h"
int
main(int argc, char **argv)
{
int sockfd, n;
char recvline[MAXLINE + 1];
struct sockaddr_in servaddr;
if (argc != 2)
err_quit("usage: a.out <IPaddress>");
if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
err_sys("socket error");
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(13); /* daytime server */
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
err_quit("inet_pton error for %s", argv[1]);
if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0)
err_sys("connect error");
struct sockaddr_in ss;
char buff[MAXLINE];
socklen_t len;
len =sizeof (ss);
if (getsockname(sockfd,(SA*) &ss,&len) < 0) printf("错误喽!");
else
{
printf("connection from %s,port %d\n",inet_ntop(AF_INET,&ss.sin_addr,buff,sizeof (buff)),
ntohs(ss.sin_port));
}
while ( (n = read(sockfd, recvline, MAXLINE)) > 0) {
recvline[n] = 0; /* null terminate */
if (fputs(recvline, stdout) == EOF)
err_sys("fputs error");
}
if (n < 0)
err_sys("read error");
exit(0);
}
5.2
#include "unp.h"
#pragma clang diagnostic push
#pragma ide diagnostic ignored "EndlessLoop"
int
main(int argc, char **argv)
{
int listenfd,confd;
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);
while (1)
{
clilen =sizeof (cliaddr);
confd = Accept(listenfd,(SA*) &cliaddr,&clilen);
if((childpid = Fork()) == 0)
//fork为每个客户派生一个处理它们的子进程
//紫禁城关闭监听套接字,父进程关闭已连接套接字,紫禁城接着调用str_echo处理客户
{
Close(listenfd);
str_echo(listenfd);
exit(0);
}
Close(confd);
}
}
#pragma clang diagnostic pop
TCP回射客户程序
#include "unp.h"
#pragma clang diagnostic push
#pragma ide diagnostic ignored "EndlessLoop"
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
if (argc !=2 )
err_quit("请输入IP地址。");
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);//将IP地址转换为二进制机器可读
Connect(sockfd,(SA*) &servaddr,sizeof (servaddr));
str_cli(stdin,sockfd);//回射函数
exit(0);
}
#pragma clang diagnostic pop
僵死进程:
(1)概念: 进程实体已经释放,但进程对应的PCB进程控制块(进程描述符)还在.
(2)产生的条件: 子进程比父进程结束的早,且父进程没有调用 wait() 获取子进程的退出码,这时子进程就变为僵死进程.
(3)若子进程比父进程结束的晚,则在父进程结束后,子进程的父进程会变成pid为1 的进程
最终版本:
#include "unp.h"
int
main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
void sig_chld(int);
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);
/*
按我现阶段的理解:
(1)Signal捕获信号SIGCHLD,并交给sig_chld处理
(2)Signal第二个参数实际上是一个函数指针,将sig_chld传入*/
/*
* sig_chld()函数解析:
* 使用waitpid函数,并指定WNOHANG参数(有尚未终止的子进程是不要阻塞)
* 为什么不适用wait函数,防止产生僵死进程,因为没有办法防止wait在正在运行的子进程尚有未终止阻塞(具体参见《unix网络编程》110页)
* 阻塞:此处的阻塞不同于此前使用的同名词语,这里的阻塞是指阻塞信号或某个信号集,防止他们在阻塞期间提交。
* 本节的目的是示范网络编程中可能遇到的情况:
* (1)当fork某个进程时,必须捕获SIGCHLD信号
* (2)当捕获信号时,必须处理被中断的系统调用
* (3)SIGCHLD的信号处理函数必须正确编写,应使用waitpid函数以免留下僵死进程
*/
Signal(SIGCHLD, sig_chld); /* must call waitpid() */
for ( ; ; )
{
clilen = sizeof(cliaddr);
if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
if (errno == EINTR)
continue; /* back to for() */
else
err_sys("accept error");
}
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 */
}
}
select()函数
#include "unp.h"
#pragma clang diagnostic push
#pragma ide diagnostic ignored "EndlessLoop"
void str_cli(FILE *fp, int sockfd) {
//需要检查的文件描述符个数
int maxfdp1;
/* fd_set其实这是一个数组的宏定义,实际上是一long类型的数组,
* 每一个数组元素都能与一打开的文件句柄(socket、文件、管道、设备等)建立联系,建立联系的工作由程序员完成,
* 当调用select()时,由内核根据IO状态修改fd_set的内容,
* 由此来通知执行了select()的进程哪个句柄可读。*/
fd_set rset;
char sendline[MAXLINE], recvline[MAXLINE];
FD_ZERO(&rset);
while (1) {
//将fd加入set集合
/*
* FD_CLR(int fd, fd_set *fdset); //将fd从set集合中清除
*FD_ISSET(int fd, fd_set *fdset); //检测fd是否在set集合中,不在则返回0
*FD_ZERO(fd_set *fdset); //将set清零使集合中不含任何fd
*/
//fileno()用来取得参数stream 指定的文件流所使用的文件描述词
FD_SET(fileno(fp), &rset);
//将套接字描述符加入集合
FD_SET(sockfd, &rset);
//为什么要加1呢:我们需要描述符的个数,而不是最大值,而描述符是从0开始的(妙吧!)
maxfdp1 = max(fileno(fp), sockfd )+ 1;
/*int select(int nfds, fd_set* readset, fd_set* writeset, fe_set* exceptset, struct timeval* timeout);
* 参数:
nfds 需要检查的文件描述字个数
readset 用来检查可读性的一组文件描述字。
writeset 用来检查可写性的一组文件描述字。
exceptset 用来检查是否有异常条件出现的文件描述字。(注:错误不包括在异常条件之内)
timeout 超时,填NULL为阻塞,填0为非阻塞,其他为一段超时时间
返回值:
返回fd的总数,错误时返回SOCKET_ERROR*/
Select(maxfdp1, &rset, NULL, NULL, NULL);
//FD_ISSET(int fd, fd_set *fdset); //检测fd是否在set集合中,不在则返回0
if (FD_ISSET(sockfd, &rset)) {
if (Readline(sockfd, recvline, MAXLINE) == 0)
err_quit("服务器超时");
Fputs(recvline, stdout);
}
if (FD_ISSET(fileno(fp), &rset)) {
if (Fgets(sendline, MAXLINE, fp) == NULL)
return;
Write(sockfd, sendline, strlen(sendline));
}
}
}
#pragma clang diagnostic pop
/* include fig01 */
#include "unp.h"
#pragma clang diagnostic push
#pragma ide diagnostic ignored "EndlessLoop"
int
main(int argc, char **argv)
{
int i, maxi, maxfd, listenfd, connfd, sockfd;
int nready, client[FD_SETSIZE];
ssize_t n;
fd_set rset, allset;
char buf[MAXLINE];
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);
maxfd = listenfd; /* 初始化 */
maxi = -1; /* index into client[] array */
//将所有描述符初始化为-1
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1; /* -1 indicates available entry */
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
/* end fig01 */
/* include fig02 */
for ( ; ; ) {
rset = allset; /* structure assignment */
//等待某个事件的发生
nready = Select(maxfd+1, &rset, NULL, NULL, NULL);
if (FD_ISSET(listenfd, &rset)) { /* new client connection */
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
#ifdef NOTDEF
printf("new client: %s, port %d\n",
Inet_ntop(AF_INET, &cliaddr.sin_addr, 4, NULL),
ntohs(cliaddr.sin_port));
#endif
for (i = 0; i < FD_SETSIZE; i++)
if (client[i] < 0) {
client[i] = connfd; /* 保存描述符 */
break;/*保存了就跳出*/
}
if (i == FD_SETSIZE)
err_quit("too many clients");
FD_SET(connfd, &allset); /*添加新的描述符来设置*/
if (connfd > maxfd)
maxfd = connfd; /* for select */
if (i > maxi)
maxi = i; /* max index in client[] array */
if (--nready <= 0)
continue; /* no more readable descriptors */
}
for (i = 0; i <= maxi; i++) { /* check all clients for data */
if ( (sockfd = client[i]) < 0)
continue;
if (FD_ISSET(sockfd, &rset)) {
if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
/*4connection closed by client */
Close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;
} else
Writen(sockfd, buf, n);
if (--nready <= 0)
break; /* no more readable descriptors */
}
}
}
}
/* end fig02 */
#pragma clang diagnostic pop
pselect()函数
函数select()是用一种超时轮循的方式来查看文件的读写错误可操作性。在Linux下,还有一个相似的函数pselect()。
poll()函数
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:
监视并等待多个文件描述符的属性变化
参数:
fds:指向一个结构体数组的第0个元素的指针,每个数组元素都是一个struct pollfd结构,用于指定测试某个给定的fd的条件
struct pollfd{
int fd; //文件描述符
short events; //等待的事件
short revents; //实际发生的事件
};