UNIXC002 TCP编程基础

1. 科普

1. TCP/UDP的优缺点

在这里插入图片描述
在这里插入图片描述

  • TCP是基于连接的,而UDP是基于数据包的。

2. 使用场景

在这里插入图片描述

2. 三次握手和四次挥手

3. 基于TCP的编程模型

在这里插入图片描述

3.1 服务器端

  • socket(2) 创建一个通讯端点,返回该端点的文件描述符 s_fd
  • bind(2)s_fd和本地地址及其端口号绑定
  • listen(2)s_fd设置为被动状态,监听连接的到来,将到来的连接放入未决连接队列中。
    • 未决连接 : 客户端已经连接成功,但服务端还没有处理的连接,以及正在处理但还没处理完的连接
  • while(1) {
    • accept(2) 从未决队列中取出一个进行处理,返回和客户端的连接描述符 c_fd。如果队列为空就阻塞等待。到这里就说明服务端的三次握手已经成功了
    • read(2)c_fd中读取客户端请求的数据。没有就阻塞等待
    • 用一些库函数等处理客户端请求的数据
    • write(2) 将处理的结果返回给客户端
    • close(2) 关闭连接描述符c_fd
  • }

3.2 客户端

  • socket(2) 创建一个通讯端点,返回该端点的文件描述符 s_fd
  • connect(2) 使用 s_fd 向服务器发起连接。到这里就说明客户端的三次握手已经成功了
  • write(2) 使用 s_fd向服务器发送请求信息
  • read(2) 使用 s_fd 阻塞等待服务器的响应消息
  • 用一些库函数处理响应的消息
  • close(2) 关闭连接描述符s_fd 结束本次连接

3.3 用到的系统调用

1. socket(2)

在这里插入图片描述

  • 这里主要讲协议家族中的 IPV4/IPV6,和通讯类型中的TCP/UDP
  • protocol 一般设置为0
$ man 2 socket
....
# domain 指定网络通信协议家族
The  domain argument specifies a communication domain; this selects the protocol family which will be used for communica‐
       tion.  These families are defined in <sys/socket.h>.  The currently understood formats include:

       Name                Purpose                          Man page
       # 本地通信域,比如linux的图形界面就采用这个AF_UNIX实现
       AF_UNIX, AF_LOCAL   Local communication              unix(7)
       # ipv4
       AF_INET             IPv4 Internet protocols          ip(7)
       # ipv6
       AF_INET6            IPv6 Internet protocols          ipv6(7)
....
# type指定通信的类型
       The socket has the indicated type, which specifies the communication semantics.  Currently defined types are:
	   # TCP
       SOCK_STREAM     Provides sequenced, reliable, two-way, connection-based byte streams.  An out-of-band  data  transmission
                       mechanism may be supported.
       # DDP
       SOCK_DGRAM      Supports datagrams (connectionless, unreliable messages of a fixed maximum length).

       SOCK_SEQPACKET  Provides  a  sequenced,  reliable, two-way connection-based data transmission path for datagrams of fixed
                       maximum length; a consumer is required to read an entire packet with each input system call.
       # 原始套接字
       SOCK_RAW        Provides raw network protocol access.
....

2. bind(2)

在这里插入图片描述

  • socket(2) 返回的文件描述符和本机地址以及端口号绑定
  • 图中 struct sockaddr 代表通用地址类型,这里的参数my_addr 的类型取决于socket(2)type参数指定的通讯类型。调用bind(2)时,要再用struct sockaddr 强制类型转化一下my_addr
    • 比如socket(2)typeSOCK_STREAM, bind(2) 中的 my_addr 的类型就是struck sockaddr_in

3. listen(2)

在这里插入图片描述

  • 收到但没处理完的也称为未决连接

3. accept(2)

在这里插入图片描述

  • addrlen 是一个值结果参数,有可能会改变(就是accept执行完以后,addrlen可能会变)

4. connect(2)

在这里插入图片描述

5. struct sockaddr

在这里插入图片描述

  • 代表通用地址类型,这是一个通用的方式,具体类型依赖于地址家族,比如全网通手机的通信服务商取决于SIM卡的类型。所以都是先用具体的协议家族,然后再转为通用的地址类型。
  • ad_data的长度不一定是14
  • 下面讲下具体的协议地址家族

3.4 地址家族

1. ipv4

在这里插入图片描述

  • 第一个值确定AF_INET ,它代表IPV4
  • 用到ipv4地址家族,需要包含头文件 #include <netinet/in.h>
$ man 7 ip
 #include <sys/socket.h>
 #include <netinet/in.h>  # 这里里面定义了IPV4的地址家族
 #include <netinet/ip.h> 
....
# 在bind调用中, 当INADDR_ANY被指定时,socket将绑定所有的本地接口
 When INADDR_ANY is specified in the bind call, the socket will be bound to all local interfaces.  .
  • sin_port是端口号,是网络字节序的 ,网络字节序一律采用大端的方式
    • 这里需要用htons(2) 把主机字节序转为网络字节序 ,才能用
    • htons(2) 是将整型变量从主机字节顺序转变成网络字节顺序, 就是整数在地址空间存储方式变为高位字节存放在内存的低地址处。
  • sin_addr是网络地址,是结构体
    • 里面的addr是网络字节序的,无符号32位的整型(我们一般看到的是点分十进制的表示方式, 需要用inet_pton(2) 把点分十进制转换为无符号32位的整型)

IPV6协议地址家族

在这里插入图片描述

  • 第一个取值固定,AF_INET6

3.6. 网络字节序和主机字节序的转换

在这里插入图片描述

3.7 inet_pton(2) 和 inet_ntop(2)

在这里插入图片描述

  • 文本格式转换二进制格式
    • 127.0.0.1 -------> 1111111 00000000 00000000 0000001

在这里插入图片描述

4. 代码示例

  • 服务端将客户端请求的字符串转为大写,打印客户端IP,并回应客户端
  • 客户端向服务端发送字符串,并将服务端返回的结果打印出来。
  • 并且是一次连接,多次交互的。

server.c

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <string.h>

#define E_MSG(STRING, VAL) do{perror(STRING); return(VAL);}while(0)

int main(void){
    // 从连接描述符中读取到的数据放到buf中
    char buf[128];
    // IP放客户端的IP
    char IP[32]; 
    // 定义并初始化地址,地址后面bind(2) 要用
    struct sockaddr_in serv, cli;
    socklen_t cli_len;
    // 创建一个通讯端点,返回该断点的文件描述符
    int s_fd =  socket(AF_INET, SOCK_STREAM, 0);
    if(s_fd == -1)E_MSG("socker", -1);
    // 对serv变量的成员进行初始化,sin_family 要和socket(2)的domain参数类型对应上。主机字节序端口号需要转为网络字节序
    serv.sin_family = AF_INET;
    serv.sin_port=htons(55579);
    // 绑定地址时,本机可能会有多个ip,服务器端需要监听所有的IP地址
    serv.sin_addr.s_addr = htons(INADDR_ANY);

    // 将s_fd和本地地址,端口号进行绑定,把ipv4类型的地址转为通用类型
    int b = bind(s_fd, (struct sockaddr *)&serv, sizeof(serv));
    if(b == -1)E_MSG("bind", -1);
    
    // 将s_fd 设置为被动连接,监听客户端连接的到来,并放入未决连接队列中
    // 指定未决连接队列的长度
    listen(s_fd, 5);
    while (1)
    {   
        cli_len = sizeof(cli);
        // 从s_fd 设备的未决连接队列中提取一个进行处理,并返回连接描述符
        // 使用这个连接描述符和客户端进行通讯
        // &cli_len 是值结果参数,有可能会被改变,结果是cli结构体的长度
        int c_fd = accept(s_fd, (struct sockaddr *)&cli, &cli_len);
        if(c_fd == -1)E_MSG("accept", -1);
        // 到这里,三次握手已经ok了,就可以进行数据的传输了
        // 打印下客户端的IP, binary --> text
        inet_ntop(AF_INET, &cli.sin_addr, IP, 32);
        printf("client IP: %s\n", IP);
        while(1){
            // 从c_fd中读取客户端发送过来的请求信息,一次只能处理128个字节,阻塞等客户端请求
            int r = read(c_fd, buf, 128);
            // 处理客户端请求的信息
            for (int i=0; i < r; i++){
                buf[i] = toupper(buf[i]);
            }
            // 将结果送回客户端
            write(c_fd, buf, r);
            if(strcmp(buf, "EXIT")==0)break;
        }
        // 关闭本次连接
        close(c_fd);
    }
    return 0;
}

client.c

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define E_MSG(STRING, VAL) do{perror(STRING); return(VAL);}while(0)


int main(int argc, char *argv[]){
    char buf[128]; // 接受服务端传来的数据
    // char *msg = "this is a test ....\n";
    char msg[128];
    struct sockaddr_in serv; //定义初服务器地址变量
    // 创建socket设备,返回设备的文件描述符
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1)E_MSG("socket", -1);
    // 服务器的信息初始化
    serv.sin_family = AF_INET;
    serv.sin_port=htons(55579);
    // text ---> binary, 结果放在serv.sin_addr
    inet_pton(AF_INET, argv[1], &serv.sin_addr);
    // 使用fd向服务器发起连接
    int c = connect(fd, (struct sockaddr *)&serv, sizeof(serv));
    if(c==-1)E_MSG("connect", -1);
    //到这里三次握手就成功了

    while(1){
        gets(msg, 20);//会把回车转为'/0' 

        // 向服务器发送请求信息,  strlen(3) 返回的长度不带'\0'
        write(fd, msg, strlen(msg) + 1);
        // 阻塞等待服务器的响应
        int r =  read(fd, buf, 128);
        if(strcmp(buf, "EXIT")==0)break;
        // 将相应信息输出到显示器
        write(1, buf, r);
        printf("\n");
    }
    // 关闭文件描述符
    close(fd);
    return 0;
}
  • 下图右边最后一行客户端吧再去访问服务端,服务端没有回应,因为还在上一个循环中没有出来,这次请求会被放入未决连接队列中。
    在这里插入图片描述
  • 下图右边第一次访问中,在最后输入 exit ,服务端收到以后就会退出这次连接,accpet 阻塞等待下一个连接到来.
    在这里插入图片描述
  • 这样就每次只能处理一个客户端的请求,不好,改进一下。参见:TCP并发服务器的实现

4. bind: Address already in use

  • 有时会遇到 bind: Address already in use 错误,这是因为内核中的地址和端口号还没有释放,得等一会。可以用setsockopt(2) 来解决。

5. 封装

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值