玩命加载中 . . .

4.2-TCP


TCP通信流程

套接字函数

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>  // 包含了前2个

>>>>>>>>>>>>>>>>>>>>服务器端<<<<<<<<<<<<<<<<<<<<

int socket(int domain, int type, int protocol);
// 创建一个套接字
- domain: 协议族,AF_INET/AF_INET6
- type: 通信协议类型,SOCK_STREAM/SOCK_DGRAM
- protocol: 具体协议,一般给0
    - SOCK_STREAM 默认TCP
    - SOCK_DGRAM 默认UDP
// 成功返回文件描述符,失败返回-1
=============================================

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 将fd和本地IP以及端口号绑定,命名socket
- sockfd: socket()得到的文件描述符
- addr: 需要绑定的socket地址,封装了IP和端口号
- addrlen: 第二个参数的大小
// 成功返回0,失败返回-1
=============================================

int listen(int sockfd, int backlog);
// 监听这个socket上的连接
- socket: 同上
- backlog: 未连接和已连接的和的最大值 // 不能超过/proc/sys/net/core/somaxconn 4096
// 成功返回0,失败返回-1
=============================================

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 接收客户端连接,阻塞
- sockfd: 同上
- addr: 传出参数,连接成功后客户端的地址信息
- addrlen: 指定第二个参数的内存大小
// 成功返回用于通信的文件描述符,失败返回-1 
>>>>>>>>>>>>>>>>>>>>客户端<<<<<<<<<<<<<<<<<<<<

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 客户端连接服务器
- sockfd: 用于通信的文件描述符
- addr: 客户端要连接的服务器的地址信息
- addrlen: 第二个参数的内存大小
// 成功返回0,失败返回-1
=============================================

ssize_t write(int fd, const void *buf, size_t count);
// 写数据
=============================================

ssize_t read(int fd, void *buf, size_t count);
// 读数据

服务器端

1. 创建一个用于监听的套接字
    - 监听:监听客户端的连接
    - 套接字:就是一个文件描述符
2. 将这个监听的文件描述符和本地的IP和端口绑定
3. 设置监听,监听的fd开始工作
4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字
5. 开始通信,接收数据,发送数据
6. 通信结束,断开连接
// TCP通信的服务器端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {
    // 1. 创建用于监听的套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);

    // 2. 绑定
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    // inet_pton(AF_INET, "10.37.62.58", &saddr.sin_addr.s_addr);点分十进制字符串转为网络字节序整数
    saddr.sin_addr.s_addr = INADDR_ANY;
    saddr.sin_port = htons(9999);   // 主机字节序端口转为网络字节序

    int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));

    // 3. 监听
    ret = listen(lfd, 8);

    // 4. 接收客户端连接
    struct sockaddr_in clientaddr;
    int clen = sizeof(clientaddr);
    int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &clen);

    // 5. 输出客户端信息
    char clientIP[16];
    inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
    unsigned short clientPort = ntohs(clientaddr.sin_port);

    printf("client IP: %s, client Port: %d\n", clientIP, clientPort);

    // 获取客户端的数据
    char recvBuf[1024] = {0};
    while (1) {
        int len = read(cfd, recvBuf, sizeof(recvBuf));
        if (len == -1) {    // 报错
            perror("read");
            exit(-1);
        }
        else if (len > 0) { // 输出接收到的数据
            printf("receive data: %s\n", recvBuf);
        }
        else if (len == 0) {    // 客户端断开连接
            printf("client close\n");
            break;
        }

        // 给客户端发送数据
        char *data = "hello, I am server";
        write(cfd, data, strlen(data) + 1);
    }
    close(cfd);
    close(lfd);// 关闭文件描述符

    return 0;
}

客户端

1. 创建一个用于通信的套接字
2. 连接服务器,需要指定连接的服务器的IP和端口
3. 连接成功,和服务器通信,接收数据,发送数据
4. 通信结束,断开连接
// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {
    // 1. 创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);

    // 2. 连接服务器
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "10.37.62.58", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

    // 3. 通信
    char recvBuf[1024] = {0};
    while (1) {
        char *data = "hello, I am client";
        write(fd, data, strlen(data) + 1);

        sleep(1);

        int len = read(fd, recvBuf, sizeof(recvBuf));
        if (len == -1) {
            perror("read");
            exit(-1);
        }
        else if (len > 0) {
            printf("receive data: %s\n", recvBuf);
        }
        else if (len == 0) {// 服务器端断开连接
            printf("server close\n");
            break;
        }
    }
    close(fd);// 关闭连接
    return 0;
}

半连接

两台主机通信时,每个socket都有两个缓冲区,一个接受数据,一个发送数据,如果关闭其中一个缓冲区,比如关闭接受数据,就只能发送,称为半连接

#include <sys/socket.h>
int shutdown(int sockfd, int how);
- sockfd: 需要关闭的socket描述符
- how: 选择关闭方式
    - SHUT_RD(0): 关闭读功能
    - SHUT_WR(1): 关闭写功能
    - SHUT_RDWR(2): 关闭读写功能

假设服务端传输一个文件,文件传输完后,如何告知客户端文件已经传输完毕,可以在文件末尾添加EOF,调用close可以发送EOF,但这样也会关闭服务器的输入端,假设此时客户端还要发送数据,就接收不到了,所以采用半关闭,关闭发送端,这样既能发送EOF,又能继续接收数据

端口复用

  • 防止服务器重启时之前绑定的端口还未释放
  • 程序突然退出而系统没有释放端口
#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
- sockfd: 要操作的文件描述符
- level: SOL_SOCKET(端口复用的级别)
- optname: 
    - SO_REUSEADDR
    - SO_REUSEPORT
- optval: 端口复用的值,1可以复用,0不可复用
- optlen: optval参数的大小
// 成功返回0,失败返回-1

设置端口复用要在服务器绑定端口之前

查看网络相关信息

netstat
-a: 查看所有socket
-p: 显示正在使用socket的程序名称
-n: 直接使用IP地址,不通过域名服务器
$ netstat -anp | grep 9999
tcp 0   0 0.0.0.0:9999      0.0.0.0:*       LISTEN      24713/./server
tcp 0   0 127.0.0.1:9999    127.0.0.1:36012 ESTABLISHED 24713/./server
tcp 0   0 127.0.0.1:36012   127.0.0.1:9999  ESTABLISHED 24714/./client

断开服务器端

tcp 0   0 127.0.0.1:9999    127.0.0.1:36012 FIN_WAIT2   -                   
tcp 1   0 127.0.0.1:36012   127.0.0.1:9999  CLOSE_WAIT  24714/./client

说明经过了两次挥手,服务端处于FIN_WAIT_2状态,客户端处于CLOSE_WAIT状态

此时再结束客户端,客户端会发送FIN,处于LAST_ACK状态,服务端收到FIN后回复ACK,处于TIME_WAIT状态,经过2MSL(Maximum Segment Life)之后变成ClOSE,释放端口,客户端收到ACK之后变成CLOSE

tcp 0   0 127.0.0.1:9999    127.0.0.1:36012 TIME_WAIT   -     

文章作者: kunpeng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 kunpeng !
  目录