参考书籍:《TCP/IP》网络编程,作者【韩】尹圣雨
测试环境:Ubuntu 10.10
GCC版本:4.4.5
一、套接字多种可选项
套接字可选项是分层的。IPPROTO_IP层可选项是IP协议相关事项,IPPROTO_TCP层可选项是TCP协议相关的事项,SOL_SOCKET层是套接字相关的通用可选项。
协议层 | 选项名 | 读取 | 设置 |
SOL_SOCKET | SO_SNDBUF(发送缓冲区大小) | O | O |
SO_RCVBUF(接收缓冲区大小) | O | O | |
SO_REUSEADDR | O | O | |
SO_KEEPALIVE | O | O | |
SO_BROADCAST | O | O | |
SO_OOBINLINE | O | O | |
SO_ERROR | O | O | |
SO_TYPE(套接字类型) | O | O | |
IPPROTO_IP | IP_TOS | O | O |
IP_TTL | O | O | |
IP_MULTICAST_TTL | O | O | |
IP_MULTICAST_LOOP | O | O | |
IP_MULTICAST_IF | O | O | |
IPPROTO_TCP | TCP_KEEPALIVE | O | O |
TCP_NODELAY | O | O | |
TCP_MAXSEG | O | O |
二、getsockopt & setsockopt
* 读取套接字可选项:getsockopt
头文件:#include <sys/socket.h>
函数公:读取套接字可选项
返回值:成功时返回0,失败时返回-1
函数原型:int getsockopt(int sock, int level, int optname, void* optval, socklen_t* optlen);
函数参数:
sock——用于查看套接字文件描述符
level——要查看的可选项协议层
optname——要查看的可选项名
optval——保存查看结果的缓冲地址值
optlen——向第四个参数optval传递的缓冲大小。调用函数后,该变量中保存通过第四个参数返回的可选项信息的字节数。
* 更改套接字可选项:setsockopt
头文件:#include <sys/socket.h>
函数公:更改套接字可选项
返回值:成功时返回0,失败时返回-1
函数原型:int setsockopt(int sock, int level, int optname, const void* optval, socklen_t optlen);
函数参数:
sock——用于更改套接字文件描述符
level——要更改的可选项协议层
optname——要更改的可选项名
optval——保存要更改的选项信息的缓冲地址值
optlen——向第四个参数optval传递的可选项信息的字节数
* 介绍getsockopt函数使用方法
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
void errorHandling(const char* message);
int main(void)
{
int tcpSock, udpSock;
int sockType;
socklen_t optlen;
int state;
optlen = sizeof(sockType);
tcpSock = socket(PF_INET, SOCK_STREAM, 0);
udpSock = socket(PF_INET, SOCK_DGRAM, 0);
printf("SOCK_STREAM:%d\n", SOCK_STREAM);
printf("SOCK_DGRAM:%d\n", SOCK_DGRAM);
state = getsockopt(tcpSock, SOL_SOCKET, SO_TYPE, (void*)&sockType, &optlen);
if(state)
errorHandling("getsockopt() error!");
printf("Socket type one:%d\n", sockType);
state = getsockopt(udpSock, SOL_SOCKET, SO_TYPE, (void*)&sockType, &optlen);
if(state)
errorHandling("getsockopt() errro!");
printf("Socket type two:%d\n", sockType);
return 0;
}
void errorHandling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
操作:
1)编译:gcc main.c -o getSockOpt.out,运行:./getSockOpt.out
SOCK_STREAM:1
SOCK_DGRAM:2
Socket type one:1
Socket type two:2
分析:
第20、25行获取套接字类型信息。如果是TCP套接字,将获得SOCK_STREAM常数值1;如果是UDP套接字,则获得SOCK_DGRAM的常数值2。
三、SO_SNDBUF & SO_RCVBUF
* SO_RCVBUF:输入缓冲大小相关可选项
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
void errorHandling(const char* message);
int main(void)
{
int sock;
int sendBuf;
int recvBuf;
int state;
socklen_t len;
sock = socket(PF_INET, SOCK_STREAM, 0);
len = sizeof(sendBuf);
state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&sendBuf, &len);
if(state)
errorHandling("getsockopt() error");
len = sizeof(recvBuf);
state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&recvBuf, &len);
if(state)
errorHandling("getsockopt() error");
printf("Input buffer size:%d\n", recvBuf);
printf("Output buffer size:%d\n", sendBuf);
return 0;
}
void errorHandling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
操作:gcc main.c -o getBuf.out,运行:./getBuf.out
Input buffer size:87380
Output buffer size:16384
* SO_SNDBUG:输出缓冲大小相关可选项
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
void errorHandling(const char* message);
int main(void)
{
int sock;
int sendBuf = 1024 * 3;
int recvBuf = 1024 * 3;
int state;
socklen_t len;
sock = socket(PF_INET, SOCK_STREAM, 0);
state = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&recvBuf, sizeof(recvBuf));
if(state)
errorHandling("setsockopt() error!");
state = setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&sendBuf, sizeof(sendBuf));
if(state)
errorHandling("setsockopt() error!");
len = sizeof(sendBuf);
state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&sendBuf, &len);
if(state)
errorHandling("getsockopt() error!");
printf("Input buffer size: %d\n", sendBuf);
len = sizeof(recvBuf);
state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&recvBuf, &len);
if(state)
errorHandling("getsockopt() error!");
printf("Output buffer size: %d\n", recvBuf);
return 0;
}
void errorHandling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
操作:gcc main.c -o setBuf.out,运行:./setBuf.out
Input buffer size: 6144
Output buffer size: 6144
分析:
缓冲大小的设置需谨慎处理,因此不会完全按照我们的要求进行,只是通过调用setsockopt函数向系统传递我们的要求。
四、SO_REUSEADDR
1.回顾程序:回声客户端/服务端程序
* 回声服务端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define TRUE 1
#define FALSE 0
void errorHandling(const char* message);
int main(int argc, char* argv[])
{
int servSock;
int clientSock;
char message[30];
int option;
int strLen;
socklen_t optlen, clientAddrSize;
struct sockaddr_in servAddr, clientAddr;
if(2 != argc)
{
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
servSock = socket(PF_INET, SOCK_STREAM, 0);
if(-1 == servSock)
errorHandling("socket() error!");
/*
optlen = sizeof(option);
option = TRUE;
setsockopt(servSock, SOL_SOCKET, SO_REUSEADDR, (void*)*option, optlen);
*/
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servAddr.sin_port = htons(atoi(argv[1]));
if(-1 == bind(servSock, (struct sockaddr*)&servAddr, sizeof(servAddr)))
errorHandling("bind() error!");
if(-1 == listen(servSock, 5))
errorHandling("listen() error!");
clientAddrSize = sizeof(clientAddr);
clientSock = accept(servSock, (struct sockaddr*)&clientAddr, &clientAddrSize);
if(-1 == clientSock)
errorHandling("accept() error!");
while((strLen = read(clientSock, message, sizeof(message))) != 0)
{
write(clientSock, message, sizeof(message));
write(1, message, strLen); //printf
memset(message, 0, sizeof(message));
}
return 0;
}
void errorHandling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
编译:gcc reuseadrEserver.c -o reuseadrEserver.out,运行:./reuseadrEserver.out 9190:
* 回声客户端:
#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 errorHandling(const char* message);
int main(int argc, char* argv[])
{
int sock = -1;
char message[BUF_SIZE] = {0};
int clntDataLen = -1;
struct sockaddr_in servAddr;
if(argc != 3)
{
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if(-1 == sock)
errorHandling("socket() error");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = inet_addr(argv[1]);
servAddr.sin_port = htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&servAddr, sizeof(servAddr)))
errorHandling("connect() error");
else
puts("Connected............");
while(1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
write(sock, message, strlen(message));
clntDataLen = read(sock, message, BUF_SIZE);
message[clntDataLen] = 0;
printf("Message from server: %s\n", message);
}
close(sock);
return 0;
}
void errorHandling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
编译:gcc echoClient.c -o echoClient.out,运行:./echoClient.out 127.0.0.1 9190
a)正常客户端/服务端通信:
服务端:
xxx@xxx-vm:~/workspace/network/TCPIP/09/reuseadrEserver$ ./reuseadrEserver.out 9190
123
123
123
123456
123
123
客户端:
Connected............
Input message(Q to quit): 123
Message from server: 123
Input message(Q to quit): 123
Message from server: 123
Input message(Q to quit): 123
Message from server: 123
Input message(Q to quit): 123456
Message from server: 123456
Input message(Q to quit): 123
Message from server: 123
Input message(Q to quit): 123
Message from server: 123
b)在客户端输入Q/q消息,或通过CTRL+C终止程序:
Input message(Q to quit): ^C
客户端和服务端都能正常退出。
c)通过CTRL+C终止服务端程序,再次运行服务端程序:
bind() error!
分析:
终止服务端程序后不能立刻重新运行服务端程序。需要过几分钟才可以正常运行。正常运行后,客户端需要重新和服务端建立连接才可以正常通信。
2.当服务器端程序和客户端程序建立连接后,通过CTRL+C终止服务端程序后,服务端程序无法立刻重新启动。
分析原因:客户端程序每次运行程序时都会动态分配端口号,因此无需过多关注Time-wait。通过CTRL+C终止服务器端程序后,套接字处在Time-wait过程时,相应端口是正在使用状态,导致重新启动服务端程序调用bind函数时报错。
解决办法:重新分配服务端套接字端口号(更改套接字可选项)。套接字的可选项中更改SO_REUSEADDR的状态,SO_REUSEADDR的默认值为0(假),这意味着无法分配Time-wait状态下的套接字端口号,因此需要将这个值改为1(真)。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define TRUE 1
#define FALSE 0
void errorHandling(const char* message);
int main(int argc, char* argv[])
{
int servSock;
int clientSock;
char message[30];
int option;
int strLen;
socklen_t optlen, clientAddrSize;
struct sockaddr_in servAddr, clientAddr;
if(2 != argc)
{
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
servSock = socket(PF_INET, SOCK_STREAM, 0);
if(-1 == servSock)
errorHandling("socket() error!");
///*
optlen = sizeof(option);
option = TRUE;
setsockopt(servSock, SOL_SOCKET, SO_REUSEADDR, (void*)*option, optlen);
//*/
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servAddr.sin_port = htons(atoi(argv[1]));
if(-1 == bind(servSock, (struct sockaddr*)&servAddr, sizeof(servAddr)))
errorHandling("bind() error!");
if(-1 == listen(servSock, 5))
errorHandling("listen() error!");
clientAddrSize = sizeof(clientAddr);
clientSock = accept(servSock, (struct sockaddr*)&clientAddr, &clientAddrSize);
if(-1 == clientSock)
errorHandling("accept() error!");
while((strLen = read(clientSock, message, sizeof(message))) != 0)
{
write(clientSock, message, sizeof(message));
write(1, message, strLen); //printf
memset(message, 0, sizeof(message));
}
return 0;
}
void errorHandling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
五、TCP_NODELAY
* Nagle算法
作用:Nagle算法用于TCP层,为防止因数据包过多而发生网络过载。
TCP套接字默认使用Nagle算法交换数据,因此最大限度地进行缓冲,直到收到ACK。从上图可知,减少了数据包传输。
当网络流量未受太大影响时,不使用Nagle算法要比使用它时传输速度快。
* 如何引用Nagle算法
禁用Nagle算法,只需将套接字可选项TCP_NODELAY改为1(真)。
int optVal = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*)&optVal, sizeof(optVal));
查看Nagle算法的设置状态:
int optVal;
socklen_t optLen;
optLen = sizeof(optVal);
getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*)&optVal, &optLen);