TCP/IP网络编程学习笔记(七)套接字的多种可选项

1.套接字的可选项

  • 创建套接字后,可以修改套接字特性
  • 套接字可选项分为SOL_SOCKET,IPPROTO_IP,IPPROTO_TCP三层

2.相关函数

  • getsockopt
#include<sys/socket.h>
// 功能:获取套接字可选项的信息
// 参数:
//    sock--用于查看选项套接字文件描述符
//    level--要查看的可选项的协议层
//    optname--要查看的可选项名
//    optval--保存要查看结果的缓冲地址值
//    optlen--向第四个参数optval传递的缓冲大小。调用函数后,该变量中保存通过第四个参数返回的可选项信息的字节数
// 返回值:成功时返回0,失败时返回-1
int getsockopt(int sock, int level,int optname, void* optval, socklen_t* optlen);
  • setsockopt
#include<sys/socket.h>
// 功能:设置套接字可选项
// 参数:
//    sock--用于更改可选项的套接字文件描述符
//    level--要更改的可选项协议层
//    optname--要更改的可选项名
//    optval--保存要更改的选项信息的缓冲地址值
//    optlen--向第四个参数optval传递的可选项信息的字节数
// 返回值:成功时返回0,失败时返回-1
int setsockopt(int sock, int level, int optname, const void* optval, socklen_t optlen);

3.获取sock_type

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>

void error_handling(char* message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(int argc, char* argv[]) {
    int tcp_sock, udp_sock;
    int sock_type;

    socklen_t optlen = sizeof(sock_type);
    tcp_sock = socket(PF_INET, SOCK_STREAM, 0);
    udp_sock = socket(PF_INET, SOCK_DGRAM, 0);
    printf("SOCK_STREAM: %d \n", SOCK_STREAM);
    printf("SOCK_DGRAM: %d \n", SOCK_DGRAM);

    int state = getsockopt(tcp_sock, SOL_SOCKET, SO_TYPE, (void*)&sock_type, &optlen);
    if (state)
        error_handling("getsockopt() error!");
    printf("Socket type one: %d \n", sock_type);

    state = getsockopt(udp_sock, SOL_SOCKET, SO_TYPE, (void*)&sock_type, &optlen);
    if (state)
        error_handling("getsockopt() error!");
    printf("Socket type two: %d \n", sock_type);

    return 0;
}

运行结果:sock_type只能获取,不能设置;套接字类型只能在创建时指定,不能修改。

TCP/IP网络编程学习笔记(七)套接字的多种可选项

4.获取并修改套接字缓冲大小

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>

void error_handling(char* message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(int argc, char* argv[]) {
    int sock;
    int snd_buf, rcv_buf;

    sock = socket(PF_INET, SOCK_STREAM, 0);

    socklen_t len = sizeof(snd_buf);
    int state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, &len);
    if (state)
        error_handling("getsockopt() error");

    len = sizeof(rcv_buf);
    state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, &len);
    if (state)
        error_handling("getsockopt() error");

    printf("Default input buffer size: %d \n", rcv_buf);
    printf("Default output buffer size: %d \n", snd_buf);

    snd_buf = 1024 * 3;
    rcv_buf = 1024 * 3;

    state = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, sizeof(rcv_buf));
    if (state)
        error_handling("setsockopt() error");

    state = setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, sizeof(snd_buf));
    if (state)
        error_handling("setsockopt() error");

    len = sizeof(snd_buf);
    state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, &len);
    if (state)
        error_handling("getsockopt() error");

    len = sizeof(rcv_buf);
    state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, &len);
    if (state)
        error_handling("getsockopt() error");

    printf("After setting, input buffer size: %d \n", rcv_buf);
    printf("After setting, output buffer size: %d \n", snd_buf);

    return 0;
}

运行结果:设置的IO缓冲大小和指定的3K不一致,这个是由系统自动调整的

TCP/IP网络编程学习笔记(七)套接字的多种可选项

5.Time-wait状态

  • 通信中,一般是客户端先请求断开链接,向服务器端发送FIN并经过四次“挥手”过程,然后会进入time-wait状态;输入ctrl+c强制结束程序,此时由操作系统关闭文件和套接字,相当于调用close函数,也会发送FIN。
  • 如果是服务器端在通信过程中强制结束程序,立即再次使用同一端口运行服务器端程序,将会输入“bind() error",无法再次运行。等待几分钟才可再次运行。
  • 原因分析:先断开连接的(先发送FIN)主机会经过Time-wait状态,此时相应端口仍是在使用状态,调用bind()使用同一端口会出错。
  • 客户端套接字的端口号是自动分配的,故无需考虑客户端的Time-wait状态
  • 为什么会有Time-wait状态?假设主机A进行发送最后一个ACK包后立即消除套接字,但这条ACK消息在传输过程中丢失,未能传输给主机B,主机B会以为它之前发送的消息没有送达主机A,试图重传,但此时A已经完全终止,无法接收主机B最后传来的ACK。有了Time-wait状态,主机A就会像主机B重传最后的ACK消息,主机B也可以正常终止。
  • 以下是回声服务器/客户端通信过程,可以发现服务器端强行结束程序后,立即再次运行,会报错

TCP/IP网络编程学习笔记(七)套接字的多种可选项

6.Time-wait状态下的端口号再分配,修改SO_REUSEADDR选项

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>

void error_handling(char* message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(int argc, char* argv[]) {
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_addr, clnt_addr;
    char message[30];

    if (argc != 2) {
        printf("Usage : %s <port> \n", argv[0]);
        exit(1);
    }

    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
        error_handling("socket() error");

    int option = 1;
    socklen_t optlen = sizeof(option);
    setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (void*)&option, optlen);

    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(atoi(argv[1]));

    if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("bind() error");

    if (listen(serv_sock, 5) == -1)
        error_handling("listen() error");

    socklen_t clnt_addr_size = sizeof(clnt_addr);
    clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
    if (clnt_sock == -1)
        error_handling("accept() error");

    int str_len;
    // 收到客户端的消息后,将消息回送给客户端
    while ((str_len = read(clnt_sock, message, sizeof(message))) != 0) {
        write(clnt_sock, message, str_len);
        write(1, message, str_len);
    }

    close(clnt_sock);
    close(serv_sock);
    return 0;
}

运行结果:可以看出,即使服务器端强行结束程序,立即重新执行程序,没有报错

TCP/IP网络编程学习笔记(七)套接字的多种可选项

7.Naggle算法

  • TCP默认使用Naggle算法交换数据,因此最大限度地进行缓冲,直到收到ACK
  • 传输大文件数据时最好禁用Naggle算法,以提高传输速度
  • 只需把TCP_NODELAY改为1即可禁用Naggle算法

参考书籍:《TCP/IP网络编程》尹圣雨 著,金果哲 译