赞
踩
代码部分有详细的注释标记了此处的注意事项和正在做的事情
.
├── cli
├── serv
├── sockcli.c
├── sockserv.c
├── str_echo.c
├── str_echo.h
├── waitchild.c
└── waitchild.h
其中cli和serv为编译好的客户端和服务端代码
#ifndef __unp_h #include "unp.h" #endif #include "signal.h" #include "waitchild.h" #ifndef UNTITLED_STR_ECHO_H #include "str_echo.h" #endif int main(int argc, char **argv){ int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; // 创建套接字描述符 // Returns a file descriptor for the new socket, or -1 for errors. listenfd = Socket(AF_INET, SOCK_STREAM, 0); if (listenfd==-1){ exit(0); } // 参数1:协议族,此为IPv4协议 // 参数2:套接字类型,此为字节流套接字, // 参数3:一般设为0,让系统选择协议类型,不然可选类型通常有 IPPROTO_TCP、IPPROTO_UDP、IPPROTO_SCTP // 结构体初始化为0???????????????????????????????????????????????? bzero(&servaddr, sizeof(servaddr)); // 设置服务协议为IPv4 servaddr.sin_family = AF_INET; // 设置服务器协议地址,此处设置为全0,所以htonl是非必须的 servaddr.sin_addr.s_addr= htonl(INADDR_ANY); // 设置服务器协议端口 servaddr.sin_port = htons(SERV_PORT); // hton*函数可以将主机字节序的数字转为网络字节序 // 因为sa_data是需要在网络上传输的,但family不用,所以family不用转为网络字节序 // 绑定一个协议地址到一个套接字 Bind(listenfd, (SA *) &servaddr, sizeof (servaddr)); // 第一个参数为套接字描述符 // 第二个参数为将sockaddr_in指针转为 sockaddr指针,注意sockaddr_in结构体中有对sockaddr的填充 // listen函数将套接字转换为被动套接字(默认为主动套接字也就是客户端),并将套接字状态从CLOSED状态转换为LISTEN状态. Listen(listenfd, LISTENQ); // 第二个参数为最大连接数 // 注册SIGCHLD的信号处理函数 signal(SIGCHLD, wait_child); for(;;){ // 获取套接字长度 clilen = sizeof(cliaddr); // 获取已连接连接队列(已完成三次握手的连接)获取队头的连接,如果已连接链接队列为空,程序进入睡眠(如果监听套接字为默认阻塞方式) connfd = Accept(listenfd, (SA *) &cliaddr, &clilen); // 返回值为<已连接套接字> // 第一个参数为<监听套接字>,此套接字在一个进程中只存在一个,而已连接套接字不是 // 第三个参数为值-结果参数,函数执行完毕后其结果为该套接字地址结构中的准确字节数 if (connfd<0){ if (errno==EINTR){ // 因为在子进程发送信号,在处理信号函数返回时可能会出现系统中断,所以在这检测重启 continue; } else{ err_sys("serv: accept failed"); } } if ((childpid=Fork()) == 0) { // 此处判断是父进程还是子进程,如果是父进程,此处为子进程的pid;如果是子进程,此处为0 //fork函数会返回两次,一次在父进程中,一次在子进程中 //fork有两种用法 // 一种是创建一个父进程的副本进程,进行某些操作 // 一种是在创建一个副本进程(子进程)后,在子进程中执行exec*函数,这样这个子进程映像就会被替换为被exec的程序文件,而且新的程序通常从main函数执行 // 如果是子进程,执行业务函数 str_echo(connfd); // 关闭描述符,其实不关闭也可以,因为exit函数本身在内核中会将全部描述符关掉 Close(listenfd); Close(connfd); // 关闭进程 exit(0); } Close(connfd); } }
#ifndef UNTITLED_STR_ECHO_H
#define UNTITLED_STR_ECHO_H
#endif //UNTITLED_STR_ECHO_H
#ifndef __unp_h
#include "unp.h"
#endif
void str_echo(int connfd);
void simpleLogN(char* str);
#include "str_echo.h" // 这仅是一个简单的往文件写入字符串的函数,替代日志 void simpleLogN(char* str) { // 注意此处使用自己的路径 const char* filename = "/home/loubw/l.txt"; FILE* fptr = fopen(filename , "w"); if (fptr == NULL) { puts("Fail to open file!"); exit(1); } fputs(str, fptr); fputs("\n", fptr); fclose(fptr); } void str_echo(int connfd){ ssize_t n; // 用于保存read函数的返回值,获取此次读取的字节数 char buf[MAXLINE]; // buffer,用于保存read的字节 // 循环读取套接字描述符中的字节 simpleLogN("sub process is running."); while(1){ n= read(connfd, &buf, MAXLINE); // read函数为慢系统调用,可能会一直阻塞 simpleLogN("get read in..."); if (n <0 && errno==EINTR){ // errno:获得系统的最后一个错误 // 如果n小于0并且是中断错误,重新进入这个循环(重启读取) continue; } else if (n==0){ // 如果n==0说明接收到客户的FIN,读取完毕,跳出循环 simpleLogN("read over but in while."); break; } else if (n<0){ simpleLogN("read ERR n<0."); // 如果出现其他错误直接退出 err_sys("str_echo: read error"); } //如果n>0,说明读取到数据,打印到命令行,并将其写入描述符给客户端 Fputs(buf, stdout); Writen(connfd, buf, n); } simpleLogN("read over."); }
#ifndef UNTITLED_WAITCHILD_H
#define UNTITLED_WAITCHILD_H
#endif //UNTITLED_WAITCHILD_H
void wait_child(int signo);
#include "stdlib.h" #include "wait.h" #include "waitchild.h" #include "str_echo.h" #ifndef _STDIO_H #include "stdio.h" #endif void wait_child(int signo){// 本函数参数必须为传入的信号num // 进程在结束时并不是真正意义的销毁,而是调用了exit将其从正常进程变为了僵死进程, // 这样的进程不占内存,不执行,也不能被调用 // 在子进程退出时会给父进程发送SIGCHLD,如果父进程不对其进行wait,就会变成僵死进程 // 如果此时父进程被杀死,子进程就会变成孤儿进程,子进程的父进程变为init进程 // wait 和 waitpid // wait在多个SIGCHLD信号发来时候只能执行一次,而多个SIGCHLD信号没有排队机制,所以只能处理其中一个子进程 // waitpid的返回值如果>0说明还有未终止的子进程,可以再while中进行判断从而处理所有的僵死进程 int stat; pid_t pid; while ((pid = waitpid(-1, &stat, WNOHANG)) >0){ // 注意要制定WNOHANG simpleLogN("sub process is terminated."); } }
#ifndef __unp_h #include "unp.h" #endif void str_cli(FILE* fp, int connfd){ char sendline[MAXLINE], recvline[MAXLINE]; // 初始化发送给服务器的字符串和接受的字符串 while (fgets(sendline, MAXLINE, fp)!=NULL){ // 阻塞获取用户输入 Writen(connfd, sendline, strlen(sendline)); // 写入到套接字描述符发送到服务器端 // 阻塞读取服务器端的返回 if (Readline(connfd, recvline, MAXLINE)==0){ // 为什么此处是readline而服务端是read???????????????????????????????????????? // 为0说明服务器关闭,退出 err_quit("str_cli: server terminated prematurely"); } // 打印到stdout从服务器接受的字节 Fputs(recvline, stdout); } } int main(int argc, char **argv){ int sockfd; // 初始化套接字描述符 struct sockaddr_in servaddr; // 初始化socket地址结构 if (argc<2){ err_quit("usage: sockcli <Server IP>"); } sockfd = Socket(AF_INET, SOCK_STREAM, 0); // 新建套接字描述符 servaddr.sin_family=AF_INET; servaddr.sin_port= htons(SERV_PORT); Inet_pton(AF_INET, argv[1], &(servaddr.sin_addr.s_addr)); // Inet_pton 为将传入的第二个参数(点分的IP字符串)转换为网络字节序的ip地址放到最后一个参数指向的内存中 int is_connected = connect(sockfd, (SA*)&servaddr, sizeof (servaddr)); // 连接服务器,这里没有使用书中的Connect函数,因为它和原生的connect返回值不同 if (is_connected==-1){ // 连接错误报错 fprintf(stderr, "connect failed error is %s\n", strerror(errno)); exit(0); } // 进行业务处理,这里捕获用户输入 str_cli(stdin, sockfd); // 业务处理完毕退出 exit(0); }
gcc -w -o serv sockserv.c waitchild.c str_echo.c -l unp
注意,主函数的文件中引用的自己编写的头文件对应的c文件必须在编译时带上,否则会报undefined错误,其余选项的含义可以参见上篇博文:上篇
gcc -w -o cli sockcli.c -l unp
分别在两个shell中运行
./serv
./cli 127.0.0.1
大端序和小端序都是针对字节(最小存储单元)而言,不是bit
#include "stdio.h" union icunion{ short s; char c[2]; }; // 因为读取内存是从低内存往高内存读取的,所以 // 如果打印出1 2就是大端, 2 1就是小端 int main(){ short inta=0x0102; union icunion icunion_obj; icunion_obj.s = inta; for (int i=0;i<2;i++){ printf("%d\n", icunion_obj.c[i]); } }
// socket的linux定义 struct sockaddr { __SOCKADDR_COMMON (sa_); /* Common data: address family and length. */ char sa_data[14]; /* Address data. */ }; // 上面的宏 #define __SOCKADDR_COMMON(sa_prefix) \ sa_family_t sa_prefix##family // 方便进行填写的socket结构体 struct sockaddr_in { __SOCKADDR_COMMON (sin_); in_port_t sin_port; /* Port number. */ struct in_addr sin_addr; /* Internet address. */ /* Pad to size of `struct sockaddr'. */ unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof (in_port_t) - sizeof (struct in_addr)]; };
之所以出现sockaddr_in是因为sa_data这个字节数组是IP和端口的结合,不好填写,注意sockaddr_in后面补0的设计,这样保证sockaddr和sockaddr_in的内存大小是一样的
#include <stdio.h> #include "stdlib.h" struct intStruct{ int a; int b; }; struct longStruct{ int c[2]; }; int main(){ struct intStruct *i = malloc(sizeof(struct intStruct)); i->a = 1; i->b = 2; //新建intStruct变量并赋值 struct longStruct *l = (struct longStruct*) i; //强转指针 printf("%d %d \n", l->c[0], l->c[1]); // 打印出 1,2 }
客户端捕获到用户收入EOF(ctrl+D),即fgets返回值是NULL,程序退出,而程序退出时内核做的一部分工作就是关闭套接字,这导致此时客户端会发送一个FIN给服务端,此时客户端处于FIN_WAIT_1状态,四次挥手开始,而服务端以ACK响应,此时服务端在CLOSE_WAIT状态。
当服务端收到FIN时,read函数返回0,处理完业务,进程退出,关闭套接字,此时向客户端发出四次挥手的第三包FIN,此时服务端进入LAST_ACK状态,等待客户端的最后一包ACK,如果等不到就长时间的处于LAST_ACK。
客户端向服务端发送四次挥手的第四包,ACK,此时客户端进入TIME_WAIT状态,而服务端收到ACK后进入CLOSED状态,连接安全关闭。
客户端在进入TIME_WAIT状态后,等待2MSL的时间(TCP包在网络上存在的最大时间*2),如果期间没有来自服务端的第三包FIN(当服务端没有收到ACK时会重发FIN),进入CLOSED状态,连接安全关闭。
感谢此条博文: link以理解四次挥手以及TIME_WAIT的作用
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。