代码部分有详细的注释标记了此处的注意事项和正在做的事情
.
├── 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状态,连接安全关闭。
感谢此条博文: 以理解四次挥手以及TIME_WAIT的作用
因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- yrrf.cn 版权所有 赣ICP备2024042794号-2
违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务