【笔记】TCPIP网络编程笔记

目录

一、理解网络编程和套接字

二、套接字类型与协议设置

三、基于TCP的服务器端/客户端

一、理解网络编程和套接字

1.网络编程中接受链接请求的套接字创建过程:

  • 第一步:调用socket函数创建套接字
  • 第二步:调用bind函数分配IP地址和端口号
  • 第三步:listen函数转为可接受请求状态
  • 第四步:调用accept函数受理接受请求

2.网络编程中发出链接请求的套接字创建过程:

  • 第一步:调用socket函数创建套接字
  • 第二步:调用connect函数匹配IP地址和端口号并发送链接请求

3.文件描述符(Windows称文件句柄)可以看作是一个编号别名

  • 0、1、2是为标准I/O保留的描述符
  • 打开的文件按时间顺序分别赋予3、4、5……

4.C语言在Linux下打开文件

1
2
3
4
5
6
7
8
9
10
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *path, int flag);

# *path是形如/temp/linshi.txt的文件路径
# flag是文件打开模式
# 成功时返回文件描述符,失败时返回-1
# 使用open创建文件时,要加上第三个参数0666,不然自己会没权限打开,因为它不会默认linux的权限0666

常用文件打开模式

1
2
3
4
O_CREAT-文件不存在时创建文件
O_RDONLY-只读
O_WRONLY-只写
O_RDWR-读写

5.C语言在Linux下关闭文件

1
2
3
4
5
6
#include <unistd.h>

int close(int fd);

# fd是文件描述符
# 成功返回0,失败返回-1

6.C语言在Linux下将数据写入文件

1
2
3
4
5
#include <unistd.h>

ssize_t write(int fd, const void* buf, size_t nbytes);

# buf是要传输的数据的地址,nbytes是要传输的数据字节数(一般可以用sizeof(data))

7.C语言在Linux下读取文件数据

1
2
3
#include <unistd.h>

ssize_t read(int fd, void* buf, size_t nbytes);

8.Windows平台上设置头文件和库

  • 链接外部库,在CMakeLists.txt里加上以下语句链接ws2_32,注意写要在add_executable之上
    1
    link_libraries(ws2_32)
  • 头文件
    1
    2
    3
    #include <stdio.h>
    #include <stdlib.h>
    #include <winsock2.h>

9.socket代码在window与linux的差异

  • 开头要多写以下两行来初始化套接字库
    1
    2
    3
    WSADATA wsaData;

    WSAStartup(MAKEWORD(2,2), &wsaData);
  • sokcet()返回的是SOCKET对象而不是int
  • struct sockaddr_in都要改成SOCKADDR_IN
  • 同理struct sockaddr*也都要改成SOCKADDR*
  • socklen_t要改成int
  • write()要换成send(),且最后要多加一个参数0,其余参数保持不变
  • read()要换成recv(),且最后要多加一个参数0,其余参数保持不变
  • close()改成closesocket()
  • 结尾加一行注销套接字库
    1
    WSACleanup()

10.套接字在网络编程中的作用是什么?为何称它为套接字

答:套接字是网络数据传输用的软件,是用来连接网络的工具。在网络连接中套接字就像一个插口,程序插上去后就可以与连接另一方进行对话。

二、套接字类型与协议设置

1.socket的第一个参数是协议簇。理论上建立socket时是指定协议,应该用PF_xxxx,设置地址时应该用AF_xxxx,但混用也没关系因为大部分时候它俩相等。一般常用的只是PF_INET

2.socket的第二个参数是套接字类型。

  • SOCK_STREAM面向连接的套接字(形如传送带)
    • 特点:传输过程数据不会消失、按序传输数据、基于字节的、传输的数据不存在数据边界(即套接字内部有缓冲区,可能多次write的数据,一次read就能接受完全)
    • 缓存满的时候也不会丢失数据,因为此时传输端套接字会自动停止传输,即面向连接的套接字会根据接收端的状态来传数据,甚至传错还会重传
    • 套接字连接必须一一对应,面向连接的套接字只能与另一个同样特性的套接字连接
  • SOCK_DGRAM面向消息的套接字(形如高速移动的快递员)
    • 特点:强调快速传输而非传输顺序、传输的数据可能丢失或损毁、限制每次传输的数据大小、传输的数据有数据边界(分两次发送的,就必须分两次读取)

3.socket第三个参数是协议的最终选择。大部分时候可以传0,因为根据前两个参数基本能确定

  • PF_INET+SOCK_STREAM,一定是IPPROTO_TCP
  • PF_INET+SOCK_DGRAM,一定是IPPROTO_UDP

4.windows下的sokcet()返回其实也是整型,也可以用int接受,但为了保存套接字句柄还是用SOCKET接受比较好,发生错误时会返回INVALD_SOCKET(用整型接受就是-1)

5.如何实现等待一定时间后再操作?(这种让CPU执行多多余任务以延迟代码运行的方式称为”Busy Waiting”)

1
2
3
for(int i = 0; i < 3000; i++){
printf("Wait time %d \n", i);
}

地址族与数据序列

1.IPv4和IPv6的差别主要是表示IP地址所用的字节数,目前通用的还是IPv4(IPv6是为了应对IP地址耗尽问题提出的标准,还未普及)

  • IPv4(Internet Protocol version 4)4字节地址族
  • IPv6(Internet Protocol version 6)16字节地址族

2.给某公司发送数据步骤

  • 先把数据传给该公司4字节IP地址的网络地址(根据A、B、C、D类来判断前几段是网络地址)
  • 该公司的路由器再根据后几段的主机地址把数据传给目标计算机

3.路由器和交换机都可以完成外网与本地主机之间的数据交换,实际用途差别不大

4.网络类型特殊记法:

  • A类地址以0-127打头
  • B类地址以128-191打头
  • C类地址以192-223打头

5.网络接口卡NIC(即网卡)是一种数据传输设备,多台计算机发送来的数据(包含IP地址和端口号)会经过NIC然后再由系统根据端口号分配给不同套接字

  • 自定义的端口号要在1024-65535之间
  • 同类型套接字端口号不能重复,即若一个TCP套接字用了1234端口,则其他TCP套接字就不能使用1234端口了,但是可以有另一个UDP套接字使用1234端口

6.结构体sockaddr_in的成员分析

  • sin_family表示选择的协议使用的地址族,可以的值为:
    • AF_INET:IPv4协议中使用的地址族
    • AF_INET6:IPv6协议中使用的地址族
    • AF_LOCAL:本地通信UNIX协议中使用的地址族
  • sin_port保存16位端口号,以网络字节序保存
  • sin_addr保存32位IP地址,以网络字节序保存
    • 里面有个成员s_addr,所有要用.sin_addr.s_addr = xx的形式来赋值
  • sin_zero无用,只是为保证sockaddr_in后面转成sockaddr结构一致而填充的0

7.sin_port端口号的赋值

  • 为避免大端序往小端序传会颠倒问题,网络传输数据时约定统一的方式,这种约定称为网络字节序,即全部统一为大端序
  • 一般只用htons()将主机字节序转换成网络字节序即可给端口赋值(注意参数是整数,不用””),长的就用htonhl()
  • ntohs()和ntohl()则是反过来转换

8.sin_addr.s_addr网络ip地址的赋值

  • 使用函数inet_addr(),注意参数是string要有””,失败的话会返回INADDR_NONE
  • 也可以使用函数htonl(INADDR_ANY)来赋值,这种方式可以自动获得运行服务器端的计算机IP地址,一般在服务器端采用,客户端除非带有一定服务器功能否则不用
  • 服务器端也需要确定网络地址的原因是:可能会碰上有多个NIC即一个计算机多种ip地址的情形

9.执行客户端client.cpp时要多传递一个IP地址(服务端地址),如果这个IP地址是计算机本身的地址的话就称为回送地址(即服务端和客户端在同一计算机上运行)。无论什么程序,一旦使用回送地址发送数据,协议软件立即返回之,不进行任何网络传输

10.为什么不直接用sockaddr而要用sockaddr_in再转成sockaddr

答:因为sockaddr中的成员sa_data要求包含IP地址和端口号,剩余部分填充0,但这个要求对赋值地址信息而言非常麻烦,所以先采用方便赋值的sockaddr_in先赋值,再转换

四、基于TCP的服务器端/客户端

1.TCP/IP协议栈一般分四层

  • 应用层
  • 传输层(TCP或UDP层)
  • 网络层(IP层)
  • 网络接口层(链路层)

2.listen()会创建服务器端套接字(相当于等候室接待人员)

  • 第二个参数表示请求等待队列长度频繁接受请求的服务器至少应为15

3.accept()会产生用于数据I/O的套接字

  • listen收到连接请求才调用accept()
  • 返回值类型是文件描述符

4.connect()使用时会给客户端套接字分配IP和端口(服务器端会在使用bind()时分配)

  • 只有服务端接受连接请求或发生异常情况而中断连接,connect()才会返回

5.在linux下客户端向服务端传整数实验

  • 客户端传整数模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    cin >> count;
    char num[1024]; //用作缓存数组,1024是一个预估最大大小
    num[0] = (char) count;
    for(int i = 0; i < count; i++){
    int _num; //临时变量,因为只能传char[]
    cin >> _num;
    num[i*4+1] = (char) _num; //i*4意味着每个整型占4字节地址,+1意味着之前的count只占用1字节地址
    }

    # 实际上num[0] = 'count的值',num[1] = '第一个数的值',num[2]、num[3]、num[4] = '0',num[5] = ‘第二个数的值’.......依次类推
    # 注意实际上可能不是'2'这种形式而是int->char自己的编码
  • 服务端接受整数模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    char temple[1024]
    read(clnt_sock, &count, 1); //因为count只占一个字节,所以只接受长度1
    cout << "count:" << count << endl;
    int temp = 0;
    for(int i = 0; i < count; i++){
    read(clnt_sock, &temple[i*4], 4); //读数据,注意这里的数字可以大但不能小,小的话会读不全, 但太大的话顶多会第一次就读完
    }
    for(int i = 0; i < count; i++){
    cout << (int)temple[i*4] << endl; //将之前用强转编码的数据解码成int
    }

    # 如果count==3的话,实际这边一共收到14字节数据,count 1字节 + 三个数字 12字节 + 多的一个0字节

6.TCP套接字中的I/O缓冲

  • write时数据移到输出缓冲,然后通过TCP移到输入缓冲,调用read时从输入缓冲中读取数据
  • I/O缓冲在每个TCP套接字中单独存在
  • I/O缓冲在创建套接字时自动生成
  • 关闭套接字会丢失输入和输出缓冲中的数据,因为输出缓冲会一直向输入缓冲传递遗留的数据

7.缓冲空间只有50字节,服务器却发送了100字节这种问题怎么解决?

答:TCP中有滑动窗口协议(自动根据接收端窗口大小来决定发送端窗口大小),规定不会发起超过输入缓冲大小的数据传输,因此TCP不会因为缓冲溢出而丢失数据

8.socket的阻塞模式

  • 当使用 write()/send()时:
    • 首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write()/send() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write()/send() 函数继续写入数据。
    • 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write()/send() 也会被阻塞,直到数据发送完毕缓冲区解锁,write()/send() 才会被唤醒。
    • 如果要写入的数据大于缓冲区的最大长度,那么将分批写入,即先写一部分,发送过去,再写一部分
    • 直到所有数据被写入缓冲区 write()/send() 才能返回
  • 当使用 read()/recv()时:
    • 首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来
    • 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 read()/recv() 函数再次读取。
    • 直到读取到数据后 read()/recv() 函数才会返回,否则就一直被阻塞。

9.TCP内部工作报文

  • 会传一个SEQ和ACK
  • SEQ:xx表示当前传输的数据包序号为xx
  • ACK:xx表示xx-1号数据包我已经收到,下一步请传输xx号数据包
  • 一般而言ACK = 上一个接受成功SEQ的数据包序号 + 上一个接受成功的SEQ的数据包字节数 + 1

10.在linux下服务端向客户端传输文件实验(即实现FTP文件传输)

  • 服务端模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <sys/types.h>
    #include <sys/stat.h>
    #include<fcntl.h>
    ...
    int flag = open("data.txt", O_RDONLY);
    char temple[1024];
    int read_true = 1;
    while(read_true != 0 && read_true != -1){
    read_true = read(flag, &temple, 10); //这个10可以随意改
    write(clnt_sock, temple, read_true); //这个字节数建议设为read_true即读到多少字节就写多少字节,免得写入未赋值的字节出现乱码
    }
    close(flag);
  • 客户端模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <sys/types.h>
    #include <sys/stat.h>
    #include<fcntl.h>

    int flag = open("data_client.txt", O_CREAT|O_TRUNC|O_WRONLY, 0666);
    char data[1024];
    int read_true = 1;
    while(read_true != 0 && read_true != -1){
    read_true = read(sock, &data, sizeof(data)-1);
    write(flag, data, sizeof(data));
    }
    close(flag);

五、基于UDP的服务器端/客户端

1.与TCP需要多个套接字不同,UDP只需要自己有一个套接字就可以与多个主机通信

  • IP只负责让离开主机A的UDP数据包传到主机B上
  • 而最终将已经传到主机B上法人数据交给哪个UDP套接字这个任务则是由UDP来完成

2.Linux下的UDP连接与TCP不同的函数(Windows下一模一样)

  • SOCKET(PF_INET, SOCK_DRGAM, 0)
  • 不需要listen()、accept()和connect()
  • read()变成了recvfrom(server_sock,buff, sizeof(buff), (struct sockaddr*)&clnt_addr, &clnt_addr_sz),clnt_addr_sz是sizeof(clnt_addr),即融合了accept()部分参数
  • 注意!recvfrom()中的clnt_addr和clnt_sz都只是定义了但没有赋值的变量!
  • write()变成了sendto(sock, buff, sizeof(buff), (struct sockeaddr*)&serv_addr, sizeof(serv_addr)),即融合了connect()部分参数
  • 调用三次sendto()的数据,一定要用三次recvfrom()来读

3.UDP传输过程分为

  • 第一阶段:向UDP套接字注册目标IP和端口号
  • 第二阶段:传输数据
  • 第三阶段:删除UDP套接字中注册的目标地址信息

4.在反复对同一目标传数据的,采用已连接的UDP套接字会更高效

  • 方法是调用connect(),使用参数和TCP一模一样,区别只在于之前定义的sock关键字是SOCKET_DGRAM
  • 之后既可以用revfrom()、sendto()也可以按TCP那种方式用read()、write(),但本质上还是UDP传输

六、优雅地断开套接字连接

1.close()是将两个流完全断开

  • 一个流:主机输出,客户端输入
  • 另一个流:主机输入,客户端输出

2.用于半关闭的函数shutdown()

  • 头文件#include<sys/socket.h>
  • shutdown(sock, howto)
  • howto的可选值
    • SHUT_RD断开输入流,该套接字不能发送数据(windows是SD_RECIEIVE)
    • SHUT_WR断开输出流,该套接字不能发送数据(windows是SD_SEND)
    • SHUT_RDWR同时断开该套接字的输入输出流(windows是SD_BOTH)

七、域名及网络地址

1.非必须一般不会修改服务器域名,而是去修改IP地址,获取某域名指向的IP地址用

1
ping www.naver.com

2.获取当前计算机中注册的默认DNS服务器使用(Linux中还需进一步输入server),这个命令获取的地址也是路由器的管理地址,属于C类的保留ip地址

1
nslookup

3.DNS本质上是一种层次化管理的分布式数据库系统

4.利用域名获取IP地址的函数gethostbyname()和结构体hostent()

  • 头文件#include <netdb.h>
  • 定义示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include <netdb.h>

    struct honstent *host;
    host = gethostbyname(hostname); //hostname就是域名字符串

    string element1 = host->h_name; //会返回官方域名,一般代表主页
    vector<string> element2 = host->h_aliases; //会返回除了官方域名以外,指向该ip的域名数组
    string element3 = host->h_addrtypr; //IPv4返回AF_INET,IPv6返回AF_INET6
    int element4 = host->h_length; //返回IP地址的字节长度,IPv4返回4,IPv6返回16
    vector<string> element5 = host->h_addr_list; //返回该域名对应的IP地址数组

5.利用IP地址获取域名的函数gethostbyaddr()

  • 头文件还是#include <netdb.h>
  • 要用到sockaddr_in结构体来赋ip地址
  • 定义示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <netdb.h>

    struct honstent *host;
    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_addr.s_addr = inet_addr(ip); //ip就是ip地址字符串
    host = gethostbyname((char*)&addr.sin_addr, 4, AF_INET); //IPv6就是6和AF_INET6

    # 结构体成员内容与gethostbyname()一模一样
    string element1 = host->h_name; //会返回官方域名,一般代表主页
    vector<string> element2 = host->h_aliases; //会返回除了官方域名以外,指向该ip的域名数组
    string element3 = host->h_addrtypr; //IPv4返回AF_INET,IPv6返回AF_INET6
    int element4 = host->h_length; //返回IP地址的字节长度,IPv4返回4,IPv6返回16
    vector<string> element5 = host->h_addr_list; //返回该域名对应的IP地址数组

八、套接字的多种可选项

1.用于读取套接字可选项和更改可选项调用的函数

  • getsockopt()成功返回0,失败返回-1
  • setsockopt()成功返回0,失败返回-1

2.查询和更改I/O缓冲区大小

  • 查询
    1
    2
    3
    4
    5
    6
    7
    int snd_buf, rcv_buf;
    socklen_t len;

    getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, &len);
    getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, &len);

    # 此时snd_buf和rcv_buf存的就是缓冲区大小
  • 更改
    1
    2
    3
    4
    5
    6
    int snd_buf = 1024*3, rcv_buf = 1024*3;

    setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, sizeof(snd_buf));
    setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, sizeof(rcv_buf));

    # 注意缓冲实际并不会完全按我们设定的值来,因为缓冲的设置需要谨慎处理,这里只是向系统传递我们的要求

3.关闭socket程序方式(谁先发出的FIN信号,谁就要进入Time-wait状态)

  • 从客户端CRTL+C关闭,相当于客户端调用close(),正常
    • 客户端也有Time-wait状态,但一般不关注,因为客户端的端口每次都会动态随机分配
  • 从服务器端CRTL+C关闭,再用同一端口号运行服务器端时会输出bind()步骤会出错,约三分钟后恢复正常
    • 原因:在四次挥手中服务器端发送fin信息后,套接字会进入Time-wait状态,此时对应端口仍处于正在使用状态

4.更改SO_REUSEADDR可以分配处于Time-wait状态下的套接字端口号

1
2
3
int option = 1;

setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (void*)&option, sizeof(option));

5.Nagle算法(只有收到上一个数据包的ack时才发送下一数据包,等待的时候将数据放入缓冲随着下一次一起发送)

  • TCP套接字默认使用Nagle算法,可以提高网络传输效率(尽可能缓冲,减少数据包的数量)
  • 传输大文件数据时应禁用Nagle,因为使不使用Nagle都会装满输出缓冲后再发送,而使用Nagle反而会浪费时间
    1
    2
    3
    4
    5
    # 禁用Nagle

    int option = 1;

    setsockopt(sock, SOL_SOCKET, SO_NODELAY, (void*)&option, sizeof(option));

6.windows的区别就是

  • getsockopy()的&len,len类型要从socklen_t改成int
  • setsockopt()第4个参数要由(void*)&改成(char*)&

九、多进程服务器端

1.常用的并发服务器端实现方法

  • 多进程服务器:通过创建多个进程提供服务
  • 多路复用服务器:通过捆绑并统一管理I/O对象提供服务
  • 多线程服务器:通过生成与客户端等量的线程提供服务

2.调用fork()函数创建进程

  • 头文件#include <unistd.h>
  • fork会创建一个正在运行的进程的副本,内存空间也复制,而不是共享,fork函数调用后的语句都会被两个进程执行
  • 父进程的fork返回值为子进程pid,子进程fork返回值为0(用以区分父进程和子进程)
    1
    2
    3
    4
    5
    6
    7
    pid_t pid;

    pid = fork();
    if(pid == 0) print("子进程");
    else print("父进程");

    # 运行一遍该代码,会同时打印出"子进程"、"父进程"

3.销毁僵尸进程的函数(头文件都是#include <sys/wait.h>)

  • wait()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int status;

    wait(&status);

    bool falg = WIFEXITED(status);
    int temple = WEXITSTATUS(status);

    # falg存销毁的子进程是否时正常退出,temple存的是终止子进程时return或exit()传的参数
    # 若有多个子进程,需要多次调用wait()
    # 若调用wait()时没有已终止的子进程,就会产生阻塞,直到有子进程终止
  • waitpid()
    1
    2
    3
    4
    5
    6
    7
    8
    int status;
    pid_t pid = fork();


    waitpid(pid, &status, WNOHANG);

    # 若把pid改成-1则表示可以销毁任意pid的子进程, WIFEXITED(status)和WEXITSTATUS(status)用法和wait一样
    # 成功时返回pid或0,失败返回-1,不会阻塞

4.子进程的销毁方式

  • 第一步:调用exit(参数) 或 main函数中执行return 参数
  • 第二步:调用wait() 或 waitpid()

5.信号机制就是当子进程满足某条件时,父进程立马调用相关函数对其进行响应

  • 函数signal()
    • 头文件#include <signal.h>
    • 第一个参数是特殊情况条件
      • SIGCHLD:子进程终止
      • SIGINT:输入CTRL+C
      • SIGALRM:已到调用alarm函数注册的时间
        • 通过alam(2)这种形式设定时间
    • 第二个参数是满足条件时要去执行的函数
      • 该函数返回值必须是void类型
      • 该函数有一个int参数,该参数存的是signal()的第一个参数
  • 更稳定的函数sigaction()
    • 先声明结构体struct sigaction act;
    • 用act.sa_handler指定要调用的处理函数的地址
    • 初始化act的sa_mask和sa_flags
    • 用sigaction(特殊情况条件, &act, 0)注册
      1
      2
      3
      4
      5
      6
      7
      8
      9
      void timeout(int sig){}

      struct sigaction act;
      act.sa_handler = timeout;
      sigemptyset(act.sa_mask); //填充0
      act.sa_falgs = 0;
      sigaction(SIGALRM, &act, 0);

      alarm(2);

6.套接字不属于进程,而是属于操作系统,同时复制套接字会导致一个端口多个套接字使用也不合理

  • 若一个套接字存在两个文件描述符,只有这两个文件描述符都终止或销毁后,才能销毁该套接字(即套接字不能close关闭,而是close关闭它的所有文件描述符后,系统会自动关闭套接字)
  • 所有fork()后的子进程应该先close(serv_sock),此时关闭的也只是套接字的文件描述符

7.I/O分割例子:父进程执行读操作,子进程执行写操作

8.在未注册SIGINT信号的情况下输出CTRL+C,会用系统默认的事件处理器终止程序

9.使用管道pipe()进行通信(IPC就是进程之间通信)

  • 头文件是#include <unistd.h>
  • 管道和套接字一样属于操作系统,因此fork不会复制
  • 执行完pipe后,fds[0]存管道出口,fds[1]存管道入口
  • 管道里的数据是无主的,即如果子进程write后立马read,那么父进程就读不到,解决办法是write后加sleep()或再创建一个管道
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    int fds[2];

    pipe(fds)

    pid=fork();
    if(pid==0){
    write(fds[1], str, sizeof(str);
    }
    else read(fds[0], buf, sizeof(buf)-1);

    # 以上实现的是子进程向父进程传输字符串

十、用select实现I/O复用

1.I/O复用实现并发服务端,原理是时分复用,用一个服务端进程为多个客户端服务

2.select函数的调用过程

  • 设置文件描述符、指定监视范围、设置超时
  • 调用select
  • 查看调用结果

3.select设置文件描述符

  • 使用fd_set位数组来存放,1表示该文件描述符是监视对象
  • 赋值完成后要用一个新的位数组temps复制,因为调用select会改变fd_set的值
    1
    2
    3
    4
    5
    fd_set reads, temps

    FD_ZERO(&reads); //将fd_set的所有位设为0
    FD_SET(0, &reads); //将文件描述符0的fd_set位设为1,即开始监视为0的文件描述符
    FD_CLR(0, &reads); //将文件描述符0的fd_set位设为0,即不再监视为0的文件描述符

4.select设置监视范围和超时

  • 第一个参数是int,设置监视范围
    1
    int maxfd; //maxfd的值是需要监视的文件描述符的数量,一般为最新建文件描述符的值+1
  • 第二个参数是fd_set数组,用来关注是否有待读取数据的文件描述符,没有就写0
  • 第三个参数是fd_set数组,用来关注是否可无阻塞传输数据的文件描述符,没有就写0
  • 第四个参数是fd_set数组,用来关注是否发生异常的文件描述符,没有就写0
  • 第五个参数是结构体timeval,设置超时(注意,每次调用select前都要重新设置一遍)
    1
    2
    3
    struct timeval timeout;
    timeout.tv_sec = 5; //设置5秒
    timeout.tv_usec = 5000; //设置5000微妙

5.调用select(windows用法也一模一样,只不过windows的第一个参数是为了兼容性才加的,没有实际意义)

  • 原则上会把fd_set的所有位清0,但监视中的且发生变化的文件描述符除外,即调用select后还为1的文件描述符表示其发生了变化(用FD_ISSET(i, &fd_set)来判断,如果符合变化条件就返回true)
  • 返回值就是发生了特定事件的文件描述符数目,超时返回0,错误返回-1
    1
    2
    3
    #include <sys/select.h>

    int fd_num = select(fd_mad, &fd_set, &fd_set, &fd_set, &timeout);

十一、其他的I/O函数

1.send和recv函数的最后一个参数是可选项(不使用时传0)

  • MSG_OOB:用于发送紧急消息,但是并不能提高传输速率
  • OOB是指out-of-band,事实上TCP并不存在真正意义上的“带外数据”(通过完全不同的通信路径传输的数据),MSG_OOB也只是通过信号处理函数urg_handler传输1个字节(相当于一个标志,告诉我这个数据是紧急的),其他数据还是用普通方式传输,
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # 服务端中使用send函数并用MSG_OOB关键字的信息会优先发送
    send(sock, "4", strlen("4", MSG_OOB);

    # 客户端收取则通过信号机制来完成(windows用select完成)
    struct sigaction act;
    act.sa_handler = urg_handler;
    sigemptyset(&act, sa_mask);
    act.sa_falgs = 0;

    fcntl(recv_sock, F_SETOWN, getpid()); //通过fcntl可以改变已打开的文件性质
    int state = sigaction(SIGURG, &act, 0);

    void urg_handler(int signo){
    ...
    recv(recv_sock, buf, sizeof(buf)-1, MSG_OOB);
    ...
    }

  • MSG_PEEK|MSG_DONTWAIT使用这种方式的参数时:用于以非阻塞方式验证输入缓冲中是否存在接受的数据
    • MSG_PEEK调用recv函数时,即使读取了也不会删除输入缓冲中的数据,即该部分数据仍存在缓冲中可供后续读取

2.readv和writev函数用于对数据进行整合后再接受和发送,用于减少I/O调用提高通信效率

  • 头文件#include <sys/uio.h>
  • 第一个参数是int类型的文件描述符
  • 第二个参数是struct iovec类型的数组,负责存数据的保存位置和大小
  • 第三个参数是第二个参数的大小
  • 适用情况:传输数据位于不同缓冲数组时,或多次传输时
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    struct iovec vec[2];
    char buf1[]="ABCD", buf2[]="1234";
    vec[0].iov_base=buf1;
    vec[0].iov_len=3; //缓冲设为3,即会发送ABC,再发送D
    vec[1].iov_base=buf2;
    vec[1].iov_len=2; ////缓冲设为2,即会发送12,再发送34

    writev(client_sock, vec, 2); //2表示有vec[0]和vec[1]

    # 接受
    char buf1[100]={0,}, buf2[100]={0,};
    //其余与上面一模一样
    readv(client_sock, vec, 2); //2表示有vec[0]和vec[1]

十二、多播与广播

1.多播的数据传输是基于UDP完成的(所以创建的套接字需要时UDP套接字),不同的是多播是同时向多个主机传递数据

  • 每次多播服务器端都只对特定多播组发1次数据
  • 加入特定组即可接受发往该多播组的数据
  • 加入多播组可以理解为:我希望接受发往目标239.234.218.234(D类ip地址)的多播数据
  • 多播发送的数据包,由路由器复制成n份再分别发往n个客户端

2.TTL和加入多播组

  • TTL就是Time to Live,决定的是数据包传递距离,每经过一个路由器TTL值就要减一,TTL为0时该数据包只能被销毁不能再传递(设置过大的TTL会影响网络流量,设置过小的TTL会导致无法传递给目标)
  • 设置TLL通过设置套接字选项完成,协议层IPPROTO_IP,选项名IP_MULTICAST_TTL
    1
    2
    int time_live = 64;
    setsockopt(sock, IPPOTO_IP, IP_MULTICAST_TTL, (void*) &time_live, sizeof(time_live));
  • 设置加入多播组也是通过设置套接字选项完成,协议层IPPROTO_IP,选项名IP_ADD_MEMBERSHIP
    1
    2
    3
    4
    5
    struct ip_mreq join_adr;
    join\_adr.imr\_multiaddr.s\_addr = "多播ip地址";
    join\_adr.imr\_interface.s\_addr = "要加入多播组的套接字所属主机的ip地址"; //也可以直接使用INADDR_ANY

    setsockopt(sock, IPPOTO_IP, IP_ADD_MEMBERSHIP, (void*) &join_adr, sizeof(join_adr));

3.多播的发送数据和接受数据(和UDP通信一模一样)

  • 发送端
    • 套接字的ip地址要写成多播地址
    • sendto()之前要设置TTL
  • 接收端
    • 套接字的ip地址要写成多播地址
    • recvfrom()之前要设置加入多播组

4.多播是基于MBone这个虚拟网络工作的,虚拟网络指“不是实际存在的物理网络,而是由网络中的某协议产生的概念性的网络”

5.多播和广播的区别

  • 多播的对象是不同网络的主机们;广播的对象是同一网络的主机们

6.广播是向同一网络中的所有主机传输数据的方法

  • 直接广播:向xx.xx.xx.255发送数据,会将数据发送给xx.xx.xx上的所有主机
  • 本地广播:向255.255.255.255发送数据,会将数据发送给自己所在网络上的所有主机
  • 开启广播设置
    1
    2
    3
    int bcast = 1;
    setsockopt(sock, SQL_SOCKET, SO_BROADCAST, (void*)& bcast, sizeof(bcast));
    - 把发送端的ip地址改一下并把设置TTL换成开启广播设置,把接收端加入多播组去掉并ip地址填INADDR_ANY,即可把多播改成广播

十三、套接字和标准I/O

1.使用标准I/O函数时会得到系统额外的缓冲支持,这个缓冲和套接字的缓冲不是同一个

  • 使用标准I/O函数传输数据时,会经历两个输入缓冲(或两个输出缓冲)
  • 位置关系是服务端/客户端 - I/O函数缓冲 - 套接字缓冲 - 网络

2.一个最简单的复制文件写法(不是标准I/O所以不是基于缓冲的)

1
2
3
4
5
6
7
8
9
10
11
int len;
char buf[3]; //buf是暂存数组,3是每次最大的暂存长度

int fd1 = open("test.txt", O_RDONLY);
int fd2 = open("test01.txt", O_WRONLY|O_CREAT|O_TRUNC);

while(len = read(fd1, buf, sizeof(buf))) >0){
write(fd2, buf, len);
}
close(fd1);
close(fd2);

复制文件功能:基于缓冲的标准I/O写法(fopen、fgets、fputs(fputc是写单个字符)、fclose)-特别快

1
2
3
4
5
6
7
8
9
10
11
int len;
char buf[3]; //buf是暂存数组

FILE* fd1 = fopen("test.txt", "r");
FILE* fd2 = fopen("test01.txt", "w");

while(fgets(buf, 3, fp1) != NULL){
fputs(buf, fd2);
}
fclose(fd1);
fclose(fd2);

3.文件描述符和FILE指针可以互相转换

  • fdopen()用于将FILE指针转换成对应的FILE指针
    1
    fdopen(fd_file, "w");
  • fileno()用于将FILE指针转换成对应的文件描述符
    1
    fileno(fd_int);

4.标准I/O函数为了提高性能,内部提供额外的缓冲,因此需要调用fflush函数来保证立即将数据传输到客户端

  • flush()功能是冲洗流中的信息,会强迫将缓冲区内的数据写到FILE指针中
    1
    fflush(fd_file);

5.标准I/O函数优点

  • 良好移植性:所有标准函数都是按ANSIC标准定义的,支持所有操作系统(编译器)
  • 可以利用额外缓冲提高性能:本来要分10次发送的数据可以堆积到缓冲只发送1次,减少头部文件和数据向输出缓冲移动的次数

6.stderr是标准错误文件的文件描述符,可以直接往里面写数据

十四、关于I/O分流

1.I/O分流是一种思想,指的是将写入和输出的文件描述符分开

  • 全关闭:用两个指向同一套接字的file指针分别分担写入和输出的工作,但是fclose关闭任意一个file指针都会导致两个一起关闭,原理是它俩其实指向的是同一文件描述符
  • 半关闭:在创建两个指向同一套接字的file指针之前,先对该套接字复制一个额外的文件描述符即可实现,fclose关闭其中一个file指针不影响另一个
    • 复制文件描述符操作
      1
      2
      3
      4
      5
      6
      # 第一种方式
      int fd_copy = dup(fd);

      # 第二种方式,除了会指定fd_copy = xx之外和第一种方式并无区别
      int fd_copy = dup2(fd, xx);

2.会向另一端发送EOF的情形

  • 调用close()、fclose()等
  • ctrl+c终止

十五、优于select的epoll

1.select的I/O复用服务器端优点和缺陷

  • 优点:兼容性好,适用大部分操作系统;设置简单,服务端接入数少的时候优先
  • 缺陷:
    • 调用select后要对所有文件描述符暴力遍历去检查状态
    • 每次调用select都需要向系统传递一个全新的监视对象组信息(因为select会改变fd_set变量,所有要复制一个fd_set作备份)
      • 这个会造成比遍历更大的开销,因为程序向系统传递数据很造成很大负担,且无法通过优化代码解决,是硬伤
      • 因为select是监视套接字标号的函数,而套接字是由操作系统管理的,所以select必须借助操作系统才能完成功能
      • 有一种弥补方法是select仅向系统传递一次监视对象,当监视对象变化时,只通知变化的事项

2.Linux的epoll函数

  • 头文件#include <sys/epoll.h>
  • 三个函数
    • epoll_create:用于创建保存epoll文件描述符的空间
      1
      int epfd = epoll_creat(50); //50是向系统建议epoll例程的大小,现在的操作系统会完全忽略这个参数而自己设置
    • epoll_ctl:向空间注册并注销文件描述符
      • 第一个参数是用于注册监视对象的eoll例程文件描述符,即epoll_create()的返回值
      • 第二个参数表示要注册还是删除的选项
        • EPOLL_CTL_ADD意味添加
        • EPOLL_CTL_DEL意味删除,删除时第四个参数事件类型填NULL
        • EPOLL_CTL_MOD意味更改已经注册过的文件描述符的关注事件类型
      • 第三个参数是需要注册的监视对象的描述符
      • 第四个参数是监视对象的事件类型组地址,即结构体struct epoll_event变量的引用
    • epoll_wait:与select类似,等待文件描述符发生变化
      • 第一个参数是用于注册监视对象的eoll例程文件描述符,即epoll_create()的返回值
      • 第二个参数是struct epoll_event *指针变量,且要用xx=malloc(sizeof(struct epoll_event)*EPOLL_SIZE));来分配缓冲空间
      • 第三个参数是int类型,代表第二个参数中可以保存的最大事件数,即EPOLL_SIZE
      • 第四个参数是int类型,表示等待时间,默认单位是0.001秒,为-1时表示一直等待直到发生
  • 结构体struct epoll_event
    • 成员events是事件类型
      • EPOLLIN是需要读取数据的情形
      • EPOLLOUT是输出缓冲为空,可以立即发送数据的情形
      • EPOLLPRI收到OOB数据的情形
      • EPOLLRDHUP断开连接或处于半关闭的情形,用于边缘触发
      • EPOLLERR发送错误的情形
      • EPOLLET以边缘触发的方式得到事件通知
      • EPOLLONESHOT发生一次事件后相应文件描述符不再收到事件通知,因此需要用EPOLL_CTL_MOD来重新设置
    • 成员data.fd是套接字的文件描述符,表示将该套接字注册到epoll例程中

3.一个简单的epoll使用(写在服务器端,实现回声功能)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int EPOLL_SIZE = 50;

struct epoll_event event_ctl;
struct epoll_event *event_wait;

int epfd = epoll_creat(50);
event_ctl.events = EPOLLIN;
event_ctl.data.fd = serv_sock;
epoll_stl(epfd, EPOLL_CTL_ADD, serv_sock, &event_ctl);
event_wait = malloc(sizeof(struct epoll_event)*EPOLL_SIZE;
int event_cnt = epoll_wait(epfd, event_wait, EOPLL_SIZE, -1);

for(int i = 0; i < event_cnt; i++){
if(event_wait[i].data.fd == serv_sock){ //如果发生变化的对象是serv_sock,即表示有新的客户端连接进来了,要更新epoll的注册,把新客户端是否传数据加入监视名单
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
event_ctl.events = EPOLLIN;
event_ctl.data.fd = clnt_sock;
epoll_stl(epfd, EPOLL_CTL_ADD, clnt_sock, &event_ctl);
}
else{ //如果发生变化的对象不是serv_sock,即表示是新客户端开始传输数据,此时的event_wait[i].data.fd值就是客户端的套接字文件描述符,要调用接受数据的代码
str_len = read(event_wait[i].dada.fd, buf, sizeof(buf)-1);

if(str_len == 0){ //表示没有从客户端读取到数据,因为条件触发会反复注册,所有不需要用到while一次读完
epoll(epfd, EPOLL_CTL_DEL, event_wait[i].data.fd, NULL); //在监视组中不再监视该客户端
close(event_wait[i].dada.fd);
else{
write(event_wait[i].dada.fd, buf, str_len); //传回去实现回声
}
}
}
}

4.epoll事件的触发分为

  • 条件触发:默认触发方式,输入缓冲收到数据后注册,后面只要输入缓冲中不为空,就继续以该事件的方式注册(select也是条件触发)
  • 边缘触发:只有在输入缓冲收到数据时注册一次,后面不注册,因此在一次读取中应该用代码读取完所有缓冲中的数据,比起条件触发要在read()外面加上个while循环
    • 边缘触发方式应该用非阻塞方式运行的read&write函数,用阻塞方式运行的话会引起服务器长时间的停顿
      • 将套接字改为非阻塞模式的方法
        1
        2
        int flag = fcntl(fd, F_GETFL, 0);
        fcntl(fd, F_SETFL, flag|O_NONBLOCK);
      • 将EPOLL改为边缘触发的方法,事件类型后面加上|EPOLLET
        1
        event_ctl.events = EPOLLIN|EPOLLET;
      • 判断读完输入缓冲中的数据方法(如果read()返回0,需要注销监视并且关闭套接字后跳出)
        • read()返回-1
        • errno == EAGAIN; //需在头文件额外引入#include <errno.h>,里面包含了一个名为errno的全局变量

5.边缘触发的最大优势在于能够分离数据的接受和处理时间点,在实际业务中边缘触发更有可能带来高性能,但不能一定说边缘触发性能一定比条件触发好

  • 因为边缘触发只会注册一次,所有系统可以自由决定什么时候接受和处理
  • 而条件触发如果不读取的话,会一直反复注册,就算用epoll_wait()来解决的话,也会出现事件数累积的问题

十六、多线程服务器的实现

1.使用多进程实现的服务器缺点

  • 创建进程的过程会带来一定的开销
  • 进程间通信需要特殊的IPC技术(管道、信号、套接字等等都算)
  • 每秒少则数十次、多则数千次的上下文切换是创建进程时最大的开销(原因是因为只有一个CPU,为了分时使用COU需要大量上下文切换)
    • 上下文切换:当前运行进程A,但下一步需要运行进程B时,需要把A的相关信息移出内存(放到硬盘里)而把B的相关信息放进来

2.线程创建函数pthread_create()具有单独的执行流,因此需要单独定义线程的main()函数

  • 含线程的代码,在编译(g++、gcc)过程中要添加-lpthread选项声明连接线程库,这样采才能调用<pthread.h>中的函数
    1
    g++ thread01.cpp -o thread01 -lpthread
  • 头文件#include <pthread.h>
  • 第一个参数是pthread_t类型的空变量,用于保存新创建线程ID的变量地址值
  • 第二个参数是指定线程属性的参数,用NULL表示使用默认属性
  • 第三个参数是线程void*类型的main()函数名的引用
    • 一般用void* thread_main(void *arg),arg就是第四个参数传给线程的变量
  • 第四个参数是(void*)&类型,用于向线程main()里面传参数
  • 一旦使用pthread_create()后就会创建新的线程并且去执行第三个参数的函数,期间进程的int main()照常运行,所有线程相关的代码中必须适当等待线程执行,不然进程一终止线程也会终止

3.一个简单线程创建和执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* thread_main(void *arg);

int main(int argc, char *argv[]){
pthread_t t_id;
int thread_param = 5;
pthread_create(&t_id, NULL, thread_main, (void*)&thread_param);
sleep(10); //用于等待线程执行的时间
puts("end");
return 0;
}

void* thread_main(void *arg){
int i = 0;
int cnt = *((int*)arg); //重点,将传进来的arg恢复成原貌
for(int i = 0; i < cnt; i++){
sleep(1);
puts("running thread");
}
return NULL;
}

4.用于让进程等待的函数pthread_join()

  • 第一个参数是pthread_t类型
  • 第二个参数是void*类型的引用,用于保存执行线程的函数最后返回给主函数的指针
  • 调用该函数的进程(或线程)将进入等待状态,直到第一个参数的线程终止
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    int main{
    ...
    pthread_t t_id;
    void *thr_ret;
    ......
    pthread_create(&t_id, NULL, thread_main, (void*)&thread_param);
    pthread_join(t_id, &thr_ret);
    free(thr_ret); //这里就能用free释放线程main中分配的msg空间了
    return 0;
    }

    void* thread_main(void *arg){
    char *msg = (char*)malloc(sizeof(char)*50); //给msg指向的地址分配50个char大小的空间
    return (void*)msg; //把msg强转成void*指针传给pthread_join的thr_ret
    }

5.所有函数可以分为:

  • 线程安全函数:多个线程同时调用也不会引发问题,大部分标准函数是线程安全的,后缀是_r的也是线程安全的
  • 非线程安全函数:多个线程同时调用会引发问题,gethostbyname()
    所以含有线程的代码在编译时加上-D_REENTRANT表示,如果函数有线程安全版本(后缀多一个_r)的话自动调用该版本
    1
    g++ -D_REENTRANT thread01.cpp -o thread01 -lpthread

6.工作线程模型(Woker)

  • 创建多个线程
  • main()只负责输出结果,不负责具体工作
  • 具体工作分别由线程完成
  • 一个简单的分别计算1-5和6-10之和的工作线程模型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #include <pthread.h>
    void * thread_summation(void * arg);
    int sum = 0;

    int main(int argc, char *argv[]){
    pthread_t id_t1, id_t2;
    int range1[] = {1, 5};
    int range2[] = {6, 10};
    pthread_create(&id_t1, NULL,thread_summation, (void *)range1);
    pthread_create(&id_t2, NULL,thread_summation, (void *)range2);

    pthread_join(id_t1, NULL);
    pthread_join(id_t2, NULL);
    printf("result:%d \n", sum);
    return 0;
    }

    void * thread_summation(void * arg){ //重点!!此时arg指向的是数组range[]首地址
    int start = ((int*)arg)[0]; //要先把arg转换成int*指针
    int end = ((int*)arg)[1]; //然后就用[0]和[1]这样的形式来取得值,不要使用++的方法
    for(int i = start; i <= end; i++) sum += i;
    return NULL;
    }

7.线程改变全局变量值的过程

  • 线程获取全局变量当前值
  • 传到CPU进行计算获得计算后的值
  • 将计算后的值传回变量来更新其值
    第二步和第三步之间完全有可能会有另一个线程获取CPU,然后使用旧的值进行计算,最终造成结果不正确,所以线程同步很重要

8.多线程概念中的临界区

  • 定义:同时运行多个线程时会引起问题的代码块
  • 出问题的全局变量不属于临界区,因为它是一个内存区域的声明,不是多条代码语句
  • 临界区通常位于由线程运行的函数内部,通常是那些用到同一内存区域的代码语句

9.线程同步的方法:互斥量

  • 头文件就是线程的头文件#include <pthread.h>
  • lock和unlock用时会很长
  • 初始化函数pthread_mutex_init()
    • 第一个参数是pthread_mutex_t类型变量的引用
    • 第二个参数是属性,一般传NULL
  • 销毁函数pthread_mutex_destroy(),参数是pthread_mutex_t类型变量的引用
  • 锁住临界区函数pthread_mutex_lock
    • 参数是pthread_mutex_t类型变量的引用
    • 在临界区开始时调用,当执行到该行时会检测临界区内是否有其他线程,若有则等待其退出临界区
  • 释放临界区函数pthread_mutex_unlock,参数是pthread_mutex_t类型变量的引用

10.线程同步的另一种方法:信号量

  • 头文件#include <semaphore.h>
  • 初始化函数sem_init()
    • 第一个参数是sem_t类型的引用
    • 第二个参数是int类型,表示可以由多个进程共享,创建1个进程内部使用的信号量时传0
    • 第三参数是指定新创建的信号量初始值,该值在调用sem_post时会+1,调用sem_wait时会减1
  • 销毁函数sem_destroy(),参数是sem_t类型的引用
  • 释放临界区函数sem_wait
    • 参数是sem_t类型的引用
    • 当信号量的值为0时,进入阻塞,直到其他线程执行了post使得信号量的值+1
  • 释放临界区函数sem_post,参数是sem_t类型的引用

11.线程销毁函数

  • 调用pthred_join除了会等待,还会自动引导其销毁
  • 调用pthread_detach()会直接销毁,参数是pthread_t类型

十七、制作HTTP服务器端

1.HTTP服务器基本知识

  • Web服务器就是基于HTTP协议,将网页对应的文件传输给客户端的服务器端
  • 超文本就是可跳转的文本,即文件传输到浏览器上展示,而我们可以通过点击展示的文本跳转到其他页面
  • HTTP协议时基于TCP/IP实现的应用层协议
  • 浏览器属于基于套接字的客户端,当连接到任意Web服务器时,浏览器内部也会创建套接字,只不过浏览器多了一项将HTML超文本解析成友好型的视图
  • Web服务器端在响应客户端请求后,立马断开连接,即使同一客户端马上再请求,服务器端也无法辨别还是原先那个客户端,所以HTTP又称“无状态的Statekess协议”(为了弥补HTTP无法保持连接的缺点,通常采用Cookie和Session技术将之前信息保存在本地)

2.客户端向Web服务器端发送请求消息的结构

  • 请求行:GET/POST方式、网页文件路径、HTTP协议版本等,只能是1行,因此会自动识别第一行是请求行
  • 消息头:用户信息、发出请求的浏览器的信息
  • 1个空行
  • 消息体:只有POST方式才有消息体,向服务器端传递数据

3.Web服务器端响应消息的结构

  • 状态行:使用的HTTP协议版本、是否成功
  • 消息头:服务器端信息和要传输的数据的格式
  • 1个空行
  • 消息体:客户端请求的数据

C++网络编程体会

1.线程函数里面最好不要使用c语言的str库函数,容易莫名其妙core dumped

2.fopen()中的文件路径不能直接用string类型变量,因为原型是char *,所以string变量要写成xx.c_str()的形式

3.widows浏览器访问虚拟机里的web服务器设置

  • 虚拟机网络适配器设置NAT模式:用于共享主机IP地址
  • 在虚拟机里使用ifconfig查询ip地址
  • Web服务器端设置ip地址为上一步查到的地址
  • 启动Web服务
  • 在widows浏览器中输入该ip地址即可实现访问

4.localhost、127.0.0.1和本机ip地址的区别

  • localhost是指向127.0.0.1的域名
  • 127.0.0.1用于本地应用之间连接,不需要有网络
  • 本机ip是供外网进行数据交流的地址,需要网络