《TCP/IP网络编程》第6、7、8、9章知识点汇总

6. 基于UDP的服务器端/客户端

6.1 理解UDP

TCP和UDP最重要的区别在于 流控制
这里的流控制应该包含了TCP的可靠传输、流量控制、拥塞控制等机制,这些机制都是在流上实现的
TCP更可靠,UDP更高效(TCP速度一般低于UDP,当每次传输数据很大时,两者速率会接近一点)
TCP在传输数据之前要建立连接(三次握手),而UDP不用。后者不询问接收方,直接发送数据

在这里插入图片描述
IP层将数据包传给主机B,而UDP就是将自己主机收到的数据包交给正确的套接字

TCP慢于UDP的原因:
(1)收发数据前后进行的连接设置及清除过程
(2)收发数据过程中为保证可靠性而添加的流控制
因此,当收发的数据量小但需要频繁连接时,UDP比TCP高效

深入学习:TCP/IP协议的内部构造

6.2 基于UDP的服务器端和客户端

UDP无需建立连接,因此不需要TCP中的listen和accept这两个步骤

TCP是一对一的,有多少客户端套接字,服务器端就需要创建多少个对应的套接字
UDP中,服务器端和客户端都仅需要1个套接字,就可以应对所有的数据传输请求(相当于每个家只需要有一个邮筒)

6.2.1 I/O函数

在TCP中,使用read和write时,只有3个参数,不需要指定对方套接字地址。因为之前在建立连接中就已经沟通过双方地址了
在UDP中,使用的是sendto和recvfrom函数,各6个参数。因此没有连接,所以在收发时要指明对方地址

  1. sendto()
#include <sys/socket.h>
ssize_t sendto(int sock, void *buff, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen);
//成功时返回传输的字节数,失败时返回-1

sock 用于传输数据的UDP套接字文件描述符。
buff 保存待传输数据的缓冲地址值。
nbytes 待传输的数据长度,以字节为单位。
flags 可选项参数,若没有则传递0。
to 存有目标地址信息的sockaddr结构体变量的地址值。
addrlen 传递给参数to的地址值结构体变量长度。

  1. recvfrom()
#include <sys/socket.h>
ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags, struct sockaddr *from, socklen_t *addrlen);
//成功时返回传输的字节数,失败时返回-1

sock 用于接收数据的UDP套接字文件描述符。
buff 保存接收数据的缓冲地址值。
nbytes 可接收的最大字节数,故无法超过参数buff所指的缓冲大小。
flags 可选项参数,若没有则传递0。
from 存有发送端地址信息的sockaddr结构体变量的地址值。
addrlen 保存参数from的结构体变量长度的变量地址值。

6.2.2 echo服务器端/客户端

UDP没有连接,从某种角度来说无法明确区分服务器端和客户端。这里将提供服务的一方成为服务器端

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

#define BUF_SIZE 1024

void error_handling(const 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;
    int clnt_addr_size;
    int i, str_len;
    char message[BUF_SIZE];

    if(argc != 2)
    {
        error_handling("wrong argc");
        exit(1);
    }
    //1. socket
    serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
    if(serv_sock == -1)
        error_handling("socket() error!");
    //2. bind
    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!");
    //3. recvfrom和sendto直接收发数据
    while(1)
    {
        clnt_addr_size = sizeof(clnt_addr);
        str_len=recvfrom(serv_sock, message, BUF_SIZE, 0, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
        printf("receive: %s", message);
        sendto(serv_sock, message, str_len, 0, (struct sockaddr *)&clnt_addr, clnt_addr_size);
    }
    close(serv_sock);
    return 0;
}
uecho_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024

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

int main(int argc, char* argv[])
{
	int sock;
	struct sockaddr_in serv_addr, from_addr;
	char message[BUF_SIZE];
    int from_addr_size;
	int str_len;

    if(argc != 3)
    {
        error_handling("wrong argc");
        exit(1);
    }
	//1. socket
	sock = socket(PF_INET, SOCK_DGRAM, 0);
    if(sock == -1)
        error_handling("socket() error!");
	//2. 设置服务器端的地址,但不再需要connect
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
	serv_addr.sin_port = htons(atoi(argv[2]));
    //3. sendto和recvfrom收发数据,和服务器端一样
    while(1)
    {
        fputs("Input message(Q to quit): ", stdout);
        fgets(message, BUF_SIZE, stdin);
        //strcmp相等返回0
        if(!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
            break;
        
        sendto(sock, message, strlen(message), 0, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
        from_addr_size = sizeof(from_addr);
        str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr *)&from_addr, &from_addr_size);
        message[str_len] = 0;
        printf("Message from server: %s", message);
    }
	close(sock);
	return 0;
}

在客户端代码中:
str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr *)&from_addr, &from_addr_size);
这一句也可以将from_addr修改为serv_addr。用另一个from_addr是为了避免收到其他方的信息,而将serv_addr覆盖掉
服务器端没有这个问题

UDP客户端套接字地址分配

在UDP中,服务器端和客户端没有那么明显的区别

sendto函数在调用的时候,如果发现之前没有使用bind函数,那么将自动分配IP地址和随机的端口号

一般服务器端会使用bind,客户端不使用

6.3 UDP的数据边界

TCP中不存在数据边界,但UDP是具有数据边界的协议
也就是说一方多少次sendto,另一方就应该多少次recvfrom

测试

UDP服务器端

for(i=0; i<3; ++i){
    sleep(5);//延迟5秒接收,等待客户端3次发送完毕
    clnt_addr_size = sizeof(clnt_addr);
    str_len=recvfrom(serv_sock, message, BUF_SIZE, 0, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
    printf("Message %d: %s\n", i+1, message);
}

UDP客户端

char msg1[] = "Hi";
char msg2[] = "This is udp client";
char msg3[] = "Nice to meet you";
sendto(sock, msg1, strlen(msg1), 0, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
sendto(sock, msg2, strlen(msg2), 0, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
sendto(sock, msg3, strlen(msg3), 0, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

UDP结果
在这里插入图片描述
每隔5秒,读入一次,和客户端的发送结果一样

TCP结果

在这里插入图片描述
用一样含义的代码,使用TCP时,会发现服务器端一次性读完了3次传来的数据。这就是没有数据边界的限制

6.4 UDP使用connect

sendto传输数据有3个阶段
(1)向UDP套接字注册目标IP和端口号
(2)传输数据
(3)删除UDP套接字中注册的目标地址信息
其中第1和第3个步骤占整个通信过程的约1/3
UDP默认无连接,但对于要多次sendto,这样的方式影响性能

可以创建连接的套接字,只需要对UDP套接字使用 connect

connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//如果对UDP使用了connect,那么不仅可以使用sendto和recvfrom,还可以使用write和read
write(sock, message, strlen(message));

同样的,客户端3次发送,服务器端3次接收

注意:即便使用了connect,UDP也不会进行三次握手

6.5 windows平台

使用的是sendto和readfrom,其他类似

#include <winsock2.h>
int sendto(SOCKET s, const char* buf, int len, int flags, const struct sockaddr *to, int tolen);
//成功时返回传输的字节数,失败时返回SOCKET_ERROR
#include <winsock2.h>
int recvfrom(SOCKET s, char* buf, int len, int flag, struct sockaddr *from,int *fromlen) ;
//成功时返回接收的字节数,失败时返回SOCKET_ERROR

注意:

  1. recvfrom接收的大小一点要大于等于sendto/send发送的大小

  2. 关闭服务器端后,客户端按理不能再收发数据。但一个有意思的现象是,在windows中,正在连接的服务器端关闭后,客户端会找相同地址的服务器端继续连接

例如uecho_server_win断开后,这里连接到了echo_server_win(可能是之前的echo_server_win没能关掉?)

uecho_server_win.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#define BUF_SIZE 1024

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

int main(int argc, char *argv[])
{   
    WSADATA wsaData;
    SOCKET serv_sock;
    SOCKADDR_IN serv_addr, clnt_addr;
    char buf[BUF_SIZE];
    int recv_len, clnt_addr_size;

    if(argc != 2)
        error_handling("wrong argc");
    //WSAStartup
    if(WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        error_handling("WSAStartup() error");
    //socket
    serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
    if(serv_sock == INVALID_SOCKET)
        error_handling("socket() error");
    //bind
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(atoi(argv[1]));
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    if(bind(serv_sock, (SOCKADDR *)&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR)
        error_handling("bind() error");
    //收发数据
    while(1)
    {
        clnt_addr_size = sizeof(clnt_addr);
        //接收
        recv_len = recvfrom(serv_sock, buf, BUF_SIZE-1, 0, (SOCKADDR *)&clnt_addr, &clnt_addr_size);
        if(recv_len == SOCKET_ERROR)
            error_handling("recvfrom() error");
        buf[--recv_len] = 0;//删除末尾的换行符(客户端输入时的那个回车符会被传过来),增加结尾符
        printf("received from client: %s\n", buf);
        //发送
        strcat(buf, "_return");
        sendto(serv_sock, buf, recv_len+7, 0, (SOCKADDR *)&clnt_addr, sizeof(clnt_addr));
    }
    closesocket(serv_sock);
    WSACleanup();
    return 0;
}

uecho_client_win.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#define BUF_SIZE 1024

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

int main(int argc, char *argv[])
{
    WSADATA wsaData;
    SOCKET clnt_sock;
    SOCKADDR_IN serv_addr, clnt_addr;
    char buf[BUF_SIZE];
    int recv_len;

    if(argc != 3)
        error_handling("wrong argc");
    //WSAStartup
    if(WSAStartup(MAKEWORD(2, 2), &wsaData) == -1)
        error_handling("WSAStartup() error");
    //socket
    clnt_sock = socket(PF_INET, SOCK_DGRAM, 0);
    if(clnt_sock == INVALID_SOCKET)
        error_handling("socket() error");
    //connect
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(atoi(argv[2]));
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    if(connect(clnt_sock, (SOCKADDR *)&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR)
        error_handling("connect() error");
    //收发数据
    int cln_size = 0;
    while(1)
    {
        fputs("Input message (Q to quit): ", stdout);
        fgets(buf, BUF_SIZE, stdin);
        if(!strcmp(buf, "q\n") || !strcmp(buf, "Q\n"))
            break;
        send(clnt_sock, buf, strlen(buf), 0);
        recv_len = recv(clnt_sock, buf, BUF_SIZE-1, 0);
        buf[recv_len] = 0;
        printf("Message from server: %s\n", buf);
    }
    closesocket(clnt_sock);
    WSACleanup();
    return 0;
}

7. 优雅地断开套接字连接

7.1 基于TCP的半关闭

1. 单方面断开连接带来的问题

场景:主机A B正在通信。主机A发送完最后的数据后就调用close()断开了连接。此时主机A无法再接收数据,于是在收到“断开”信息之前,这期间主机B传过去的数据也就销毁了。

解决方法:Half-close,连接的半关闭。指可以传输,但无法接收。或者可以接收,但无法传输

2. 套接字和流(Stream)

A B两个主机建立连接之后,各拥有2个流,分别是输入流和输出流。A的输入流和B的输出流对接,A的输出流和B的输入流对接
Half-close 就是指断开其中的一个流连接
close()和closesocket()都是同时断开2个流

shutdwon() 函数

用于半关闭的函数shutdown,关闭其中一个流

int <sys/socket.h>
int shutdown(int sock, int howto);
//成功时返回0,失败时返回-1

sock 需要断开的套接字文件描述符
howto 传递断开方式信息

在这里插入图片描述

半关闭的理解

场景:服务器端向客户端发送一个文件,客户端收到之后返回给服务器端 “thank you”

问题1:服务器端只需要不断发送数据直至完毕,但客户端不知道接收完毕,一直 read(),等得不到数据将陷入阻塞状态
解决:约定一个文件结束符(这个结束符不能和出现在文件内容,因此只能通过单独传输一次结束符来避免)

问题2:服务器端发送了 EOF 文件结束符后,调用close()关闭连接。客户端返回的 “thank you” 无法被接收
解决:客户端在发送EOF后,只关闭输出流,保留输入流

基于半关闭的文件传输程序

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

#define BUF_SIZE 1024

int main(int argc, char* argv[]){

    int serv_sock, clnt_sock;
    FILE *fp;
    struct sockaddr_in serv_addr, clnt_addr;
    int clnt_addr_size;
    int i, read_cnt;
    char buf[BUF_SIZE];

    if(argc != 2)
    {
        printf("wrong argc\n");
        exit(1);
    }
    //打开文件
    fp = fopen("file_server.c", "rb");//r表示只读,b表示二进制(文件必须已存在)
    //socket
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    //2. bind
    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]));
    bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    //listen
    listen(serv_sock, 5);
    //accept
    clnt_addr_size = sizeof(clnt_addr);
    clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
    //读文件,发送数据
    while(1)
    {
        read_cnt = fread((void *)buf, 1, BUF_SIZE, fp);
        printf("send message %d\n", read_cnt);
        if(read_cnt<BUF_SIZE)
        {
            write(clnt_sock, buf, read_cnt);
            break;
        }
        write(clnt_sock, buf, BUF_SIZE);
    }
    //半关闭socket
    shutdown(clnt_sock, SHUT_WR);//关闭输出流,保留输入流
    read(clnt_sock, buf, BUF_SIZE);
    printf("Message from client: %s\n", buf);
    //关闭socket
    fclose(fp);
    close(clnt_sock);
    close(serv_sock);
    return 0;
}
file_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024

int main(int argc, char* argv[])
{
	int clnt_sock;
    FILE *fp;
	struct sockaddr_in serv_addr, from_addr;
	char buf[BUF_SIZE];
    int from_addr_size;
	int read_cnt;

    if(argc != 3)
    {
        printf("wrong argc");
        exit(1);
    }
    //打开文件
    fp = fopen("receive.dat", "wb");//不存在则创建,存在则覆盖
	//socket
	clnt_sock = socket(PF_INET, SOCK_STREAM, 0);
	//connect
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
	serv_addr.sin_port = htons(atoi(argv[2]));
	connect(clnt_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
    //接收文件,并返回 thank you
    while((read_cnt = read(clnt_sock, buf, BUF_SIZE)) != 0)
        fwrite((void *)buf, 1, read_cnt, fp);
    puts("Received file data");
    write(clnt_sock, "Thank you", 10);
    //关闭
    fclose(fp);
	close(clnt_sock);
    
	return 0;
}
结果和说明

上述程序能够完成之前的场景要求

shutdown(clnt_sock, SHUT_WR);//关闭输出流,保留输入流

shutdown和close的区别:

  1. shutdown总能够发送FIN报文,但close不一定
  2. close存在计数,并不一定导致该套接字不可用,shutdown不管引用计数,直接使得套接字失效,如果其他进程使用该套接字,将会受到影响。
  3. close会关闭连接,并释放所有连接对应的资源,而shutdown并不会释放套接字和所有资源

7.2 windows下的Half_close

#include <winsock2.h>
int shutdown(SOCKET sock, int howto);
//成功时返回0,失败时返回SOCKET_ERROR

注意第二个参数的值写法有所不同

在这里插入图片描述

分别以0,1,2表示,和linux下的是一样的,只是名字不同

实现:略

8. 域名及网络地址

DNS,Domain Name System,域名系统,用于IP地址和域名的相互转换,核心是DNS服务器

百度的IP地址:39.156.66.18(不唯一)
百度的域名:www.baidu.com

输入以上的任意一个都可以打开百度。但实际上,输入域名时,需要通过DNS服务器将域名转换为IP地址
所有计算机中都记录着默认DNS服务器地址,就是通过这个默认DNS服务器得到相应域名的IP地址信息

一般不会更改域名,但会更改IP地址。编写程序时应当多使用域名而非IP地址
查看域名对应的IP地址:ping www.baidu.com
查看默认DNS服务器地址:nslookup (linux中还要根据提示信息输入 server)

在这里插入图片描述

默认DNS如果找不到该域名,则逐级向上询问。最后根DNS会知道向哪个DNS服务器请求解析域名。最后解析得到的IP地址原路返回
DNS是一种分布式数据库系统

当默认DNS找到该域名时:

在这里插入图片描述

DNS是将域名转为IP地址,路由器根据IP地址选择路径。DNS和路由器是不同的概念
DNS和操作系统无关

8.1 利用域名获取IP地址 hostent

根据字符串格式的域名获取到ip地址

#include <netdb.h>
struct hostent * gethostbyname(const char *hostname);
//成功时返回hostent结构体地址,失败时返回NULL指针

hostent结构体

struct hostent
{
    char *h_name;		//official name
    char **h_aliases;	//alias list
    int h_addrtype;		//host address type
    int h_length;		//address length
    char **h_addr_list;	//address list
};
//重点是h_addr_list
h_name	官方域名,但有些公司并未用官方域名注册
h_aliases	可以通过多个城名访问同一主页。同一IP可以绑定多个域名,因此,除官方域名外还可指定其他域名
h_addrtype	获取保存在h_addr_list中的IP地址的地址族信息,如果是IPv4,则保存的是AF_INET
h_length	IP地址长度,如果是IPv4,就是4;如果是IPv6,就是16
h_addr_list	通过此变量以整数形式保存域名对应的IP地址

在这里插入图片描述

其中,h_addr_list其实是一个指针数组,数组中每个元素都是in_addr型指针

没有写成 in_addr** 是为了提高通用性,因为char *和in_addr *都是4字节的指针
现在一般用void *来处理这种情况,但当时定义套接字时是在void指针标准化之前,那时使用char *指代不明确类型

在这里插入图片描述

测试

#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <netdb.h>

int main(void)
{
    int i;
    char domain[30] = "www.baidu.com";
    struct hostent *host;

    host = gethostbyname(domain);
    if(!host)
    {
        printf("gethostbyname() error\n");
        exit(1);
    }

    //查看IP地址信息
    printf("Official name: %s\n", host->h_name);
    for(i=0; host->h_aliases[i]; ++i)
        printf("Aliases %d: %s\n", i+1, host->h_aliases[i]);
    printf("Address type: %s\n", (host->h_addrtype == AF_INET)?"AF_INET":"AF_INET6");
    printf("Address length: %d\n", host->h_length);
    for(i=0; host->h_addr_list[i]; i++)
        printf("IP addr %d: %s\n", i+1, inet_ntoa(*(struct in_addr *)host->h_addr_list[i]));
    
    return 0;
}

在这里插入图片描述

8.2 利用IP地址获取域名

#include <netdb.h>
struct hostent * gethostbyaddr(const char *addr, socklen_t len, int family);
//成功时返回hostent结构体地址,失败时返回NULL指针
addr	含有IP地址信息的in_addr结构体指针。为了同时传递IPv4地址之外的其他信息,该变量的类型声明为char指针
len		向第一个参数传递的地址信息的字节数, IPv4时为4,IPv6时为16
family	传递地址族信息,IPv4时为AF_INET,IPv6时为AF_INET6

测试

问题:
使用gethostbyaddr时,使用网上查到的百度ip地址202.108.22.5可以成功解析
但是使用gethostbyname得到的ip地址39.156.66.18返回的是NULL,尽管这一ip能ping通(原因?)

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

int main(void)
{
    int i;
    //char ip[30] = "39.156.66.18";
    //char ip[30] = "74.125.19.106";
    char ip[30] = "202.108.22.5";
    struct hostent *host;

    struct in_addr addr;
    addr.s_addr = inet_addr(ip);

    host = gethostbyaddr((char *)&addr, 4, AF_INET);
    if(!host)
    {
        printf("gethostbyname() error\n");
        perror("gethostbyaddr");
        exit(1);
    }

    printf("Official name: %s\n", host->h_name);
    for(i=0; host->h_aliases[i]; ++i)
        printf("Aliases %d: %s\n", i+1, host->h_aliases[i]);
    printf("Address type: %s\n", (host->h_addrtype == AF_INET)?"AF_INET":"AF_INET6");
    printf("Address length: %d\n", host->h_length);
    for(i=0; host->h_addr_list[i]; i++)
        printf("IP addr %d: %s\n", i+1, inet_ntoa(*(struct in_addr *)host->h_addr_list[i]));
    
    return 0;
}

在这里插入图片描述

8.3 windows平台

有差不多的函数

#include <winsock2.h>
struct hostent * gethostbyname(const char * name);
//成功时返回hostent结构体变量地址值,失败时返回NULL指针。
struct hostent * gethostbyaddr(const char *addr,int len, int type);
//成功时返回hostent结构体变量地址值,失败时返回NULL指针。

在windows平台下,代码上除了要修改头文件和添上WSA语句,其他一样;
结果也一样

9. 套接字的多种可选项

在这里插入图片描述
SOL_SOCKET 套接字相关的通用可选项
IPPROTO_IP IP协议相关事项
IPPROTO_TCP TCP协议相关事项

9.1 getsockopt 和 setsockopt 和 SNDBUF 和 RCVBUF

#include <sys/socket.h>
int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
//成功时返回0,失败时返回-1

level 协议层
optname 要查看的可选项名
optval 保存查看结果的缓冲地址值
optlen 保存optval传递的缓冲大小

#inlclude <sys/socket.h>
int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
//成功时返回0,失败时返回-1

示例

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

int main(void){
    int tcp_sock, udp_sock;
    int sock_type;
    socklen_t optlen;
    int state;

    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);
	//获取可选项
    state = getsockopt(tcp_sock, SOL_SOCKET, SO_TYPE, (void *)&sock_type, &optlen);
    if(state){
        printf("getsockopt() error!");
        exit(1);
    }
    printf("Socket type one: %d \n", sock_type);

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

    return 0;
}

在这里插入图片描述

SO_TYPE是只读选项
“套接字类型只能在创建时决定, 以后不能再更改。”

获取输入缓冲大小和输出缓冲大小

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

int main(void){
    int sock;
    int snd_buf, rcv_buf;
    socklen_t buflen;
    int state;
    //建立套接字
    sock = socket(PF_INET, SOCK_STREAM, 0);
    //获取输出缓冲
    buflen = sizeof(snd_buf);

    state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, &buflen);
    if(state){
        printf("getsockopt() error!");
        exit(1);
    }
    //获取输入缓冲
    state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, &buflen);
    if(state){
        printf("getsockopt() error!");
        exit(1);
    }
    printf("Input buffer size: %d \n" , rcv_buf);
    printf("Output buffer size: %d \n" , snd_buf);

    return 0;
}

在这里插入图片描述

setsockopt示例

//设置输入缓冲和输出缓冲
int snd_buf=1024*3, rcv_buf=1024*3;
state = setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void *)&snd_buf, sizeof(snd_buf));
state = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void *)&rcv_buf, sizeof(rcv_buf));

在这里插入图片描述

9.2 SO_REUSEADDR

time-wait
主机A率先发送断开连接请求时,在四次握手过程中,最后一个消息由主机A发送。如果这最后一个消息丢失,那么主机B没有收到确认,就会认为自己的最后一次发送(第三次握手)对方没有收到,于是发起重传。但是因为A关闭后无法收到B的重传,因此B收不到确认就会一直重传。基于这些考虑,先传输Fin消息的主机应经过Time-wait过程

客户端不考虑time-wait是因为客户端的端口一般是随机分配的
如果出现延迟、丢失,time-wait计时器不断重启,将导致服务器迟迟无法重启

time-wait存在的意义
1.保证第四次握手客户端发送的最后一个ACK报文段 能够到达服务端,可靠地实现了TCP全双工连接的终止;
2.防止“已失效的连接请求报文段”出现在本连接中

例如回声代码中,如果服务器端断开连接,那么不能立即以同样的端口号重启服务器,会出现bind() error(试了一下,发现并无影响,仍待观察)。取消time-wait只需要设置如下代码

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

SO_REUSEADDR 的默认值为 0,将其修改为 1 即可将 time-wait 状态下的套接字端口号重新分配给新的套接字

9.3 TCP_NODELAY 和 Nagle算法

Nagle算法诞生于1984年,应用于TCP层,是为了防止数据包过多而发生的网络过载

在这里插入图片描述

Nagle算法:只有收到前一数据的ACK消息,才发送下一数据
TCP套接字默认使用Nagle算法交换数据

以传输“Nagle”字符串为例(极端情况):
Nagle on:N率先被发送,等待N的ACK时,agle填入输出缓冲;拿到ACK后,将agle数据包发送。一共4个数据包
Nagle off:5个字符和5个ACK, 传输10个数据包

禁用Nagle算法可以提高传输速度,但增加了网络流量
禁用Nagle算法的场景:最典型的是“传输大文件数据”,数据传入输出缓冲不会花太多时间,这种情况即便不使用Nagle每次传输的数据包也是满的,因而不用Nagle会更好

//禁用Nagle
int optval = 1;
state = setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&opt_val, sizeof(opt_val));

同样可以用getsockopt查看Nagle使用与否
正在使用为0,已禁用为1

示例:

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

int main(void){
    int sock;
    socklen_t len;
    int state;
    //建立套接字
    sock = socket(PF_INET, SOCK_STREAM, 0);
    
    //获取默认的值
    int optval = -1;
    len = sizeof(optval);
    state = getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*)&optval, &len);
    printf("Default TCP_NODELAY value: %d \n" , optval);

    //修改,禁用Nagle算法
    optval = 1;
    state = setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*)&optval, len);
    state = getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*)&optval, &len);
    printf("Modified value: %d \n" , optval);
    
    return 0;
}

在这里插入图片描述

9.4 windows下的实现

套接字可选项及其相关内容和操纵系统无关,在linux下和windows下是一样的,如:

#include <winsock2.h>
int setsockopt(SOCKET sock, int level, int optname, const char* optval, int optlen);
//成功时返回0,失败时返回SOCKET_ERROR。

注意的是,这里optval从linux下的 void* 变为了 char*

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值