【UNIX网络编程】第五章TCP简单通讯完整代码及知识点

2 篇文章 0 订阅

代码

目的

在这里插入图片描述

代码部分有详细的注释标记了此处的注意事项和正在做的事情

目录结构

.
├── cli
├── serv
├── sockcli.c
├── sockserv.c
├── str_echo.c
├── str_echo.h
├── waitchild.c
└── waitchild.h

其中cli和serv为编译好的客户端和服务端代码

服务端代码

  • sockserv.c
#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);
    }
}

  • str_echo.h
#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);
  • str_echo.c
#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.");
}
  • waitchild.h
#ifndef UNTITLED_WAITCHILD_H
#define UNTITLED_WAITCHILD_H

#endif //UNTITLED_WAITCHILD_H

void wait_child(int signo);
  • waitchild.c
#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.");
    }
}

客户端代码

  • sockcli.c
#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

  • 最高有效位和最低有效位
    像0101 1011,最左边为最高有效位,最右边为最低有效位,类似于十进制的最高位和最低位
  • 大端序
    像0x12345678这个数,12这个字节存在内存的最低地址,78这个字节存在最高地址,这样叫做大端序,更符合人类的阅读习惯,所以网络字节序是大端序(大端先进内存),这样的话最高有效位在低内存地址
  • 小端序
    像0x12345678这个数,78这个字节存在最低内存地址,12这个字节在最高内存地址,这叫做小端序(小端先进内存),这样的话最高有效位存在高内存地址
  • 你可以通过这个程序确定你的机器是大端还是小端
#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数据结构

// 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的内存大小是一样的

  • sa_family_t 为无符号短整型,用来表示协议族
  • sa_data表示端口号和IP
  • C的结构体只是内存的组织方式,比如
#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
}

fork函数

  • fork函数会派生出一个子进程,fork函数之后,一定是两个进程同时执行fork函数之后的代码,而之前的代码以及由父进程执行完毕。这个函数在网络编程中非常常用。
  • fork函数会返回两次,父进程一次,子进程一次。子进程返回的是0而父进程返回的是创建的子进程的id。可以通过这个判断当前应该执行的代码。
  • fork还有种用法是在子进程中调用exec(),从而使子进程变为一个新的程序,新程序从被exec的可执行文件的main函数开始执行,注意之所以叫新程序而不是新进程的原因是其pid并不变。

信号处理函数与僵死进程

  • 进程的僵死状态是为了维护子进程的信息,以便将来父进程获取,包括其子进程ID、终止状态以及资源信息等。如果僵死进程的父进程被杀死,此时僵死进程被称为孤儿进程,其父进程id被置为1,也就是init进程,由init进程负责wait它们。
  • 我们在fork出子进程时都需要对其退出时发出SIGCHLD信号时对其进行wait,这样就可以防止其变为僵死进程。
  • 我们在进程中通过signal.h的signal函数注册信号处理函数。
  • wait()和waitpid():wait在多个信号同时发来(多个子进程同时退出)时,只能使其中一个子进程结束僵死状态。因为多个信号是”不排队“的(书中所言)。而waitpid可以通过返回值的方式获取到此时还有无僵死子进程,从而将其wait,具体见上面wait_child函数。

已连接套接字和监听套接字

  • 监听套接字在每个TCP服务端只有一个,其负责从Bind、Listen、到Accept的部分,当Accept函数返回,即三次握手结束后,我们就会得到一个已连接套接字,并根据这个套接字在子进程中与客户端进行交互。这就是一个服务端可以服务多个客户端的原理。已连接套接字的ip和端口记录的是对面的,而监听套接字记录的是自己的。

慢系统调用与中断处理

  • 慢系统调用是类似于accept、read等阻塞式的可能永远不会返回的调用。当进程处于此种调用的状态中,收到某些信号(或者其他情况,暂时还不清楚)比如上面main函数在accept时可能会受到子进程的SIGCHLD信号就会触发中断(错误码EINTR)。此时如果我们不处理程序就会退出,而且我们在代码中选择了直接忽略,并重新进行循环。

运行流程

正常启动

  • 服务端新建socket、bind、listen后得到一个监听套接字,此时调用accept阻塞等待进程中已连接连接队列队头的连接。
  • 客户端新建socket、指定其端口和IP(注意指定的是服务器端的),调用connect函数。
  • connect函数会与服务端进行三次握手,客户端的connect在第二次握手时得到返回值:一个已连接套接字。服务端的accept会在三次握手结束后得到返回值:-个已连接套接字。
  • 服务器端accept返回后,主进程回到accept等待下一个已连接连接,子进程开始执行业务代码,并阻塞在read函数
  • 客户端此时阻塞在fgets等待用户输入,用户输入后便调用writen将输入传输到服务器,然后进入read等待服务器返回数据,此时服务器的read读取到数据,再将数据通过writen传输给客户端。这样下次传输得以开始
  • 三次握手

正常终止

  • 客户端捕获到用户收入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的作用

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值