目录
五、 套接字可选项 getsockopt & setsockopt
前言
本系列是阅读尹圣雨所著TCP/IP网络编程一书的学习笔记,我将记录一些关键知识和遇到的问题,在最后能够自己搭建一个简易的服务器。
本文主要介绍TCP服务端和客户端的一些关键函数
一、服务器端函数
1. 创建套接字函数 socket
int socket(int domain, int type, int protocol);
调用成功则返回文件描述符,失败则返回-1。为了方便程序员指定套接字,套接字创建时操作系统会自动给套接字分配一个整数,这个整数就是文件描述符。0、1、2是标准输入输出的文件描述符。
(1)domain: 套接字中使用的协议族。常用的有PF_INET(ipv4协议族)、PF_INET6(ipv6协议族)。协议族的类型将决定protocol参数类型的范围。
(2)type: 数据传输类型。主要有SOCK_STREAM(流套接字、TCP),SOCK_DGRAM(数据报套接字、UDP),SOCK_RAW(原始套接字、用于其他协议)三种。
(3)protocol: 通信使用的协议。一般参数为0,除非domain和type都相同但是protocol不同的情况下才需要具体指定。
创建一个TCP套接字如下:
int server_socket = socket(PF_INET,SOCK_STREAM,0);
2. 套接字绑定地址函数 bind
int bind(int sockfd, struct sockaddr* addr, socklen_t addrlen);
调用成功返回0,否则返回-1。
(1)sockfd:套接字的文件描述符。
(2)addr:保存地址信息的结构体变量的地址。
(3)addrlen:结构体变量长度。一般直接用sizeof(addr)。
定义addr结构体并赋值如下:
struct sockaddr_in server_addr;
memset(&server_addr,0,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(server_ip);
server_addr.sin_port = htons(atoi(server_port));
结构体struct sockaddr_in中有四个变量:
sin_family,保存地址族信息,常用的有AF_INET(ipv4)、AF_INET6(ipv6);
sin_addr.s_addr,保存32位IP地址,inet_addr()函数将点分十进制的字符串IP地址转化为整数型,同时进行网络字节序转换。 为了统一网络传输数据的顺序,规定采用大端序传输,所以字段要经过网络字节序转换。
sin_port,保存16位端口号,htons()函数用于网络字节序转换,htons意为host to network short(用于16位端口),htonl意为host to network long(用于32位地址)。
sin_zero[8],不使用,必须填充0。所以使用memset()函数给结构体赋初值0。
调用bind函数如下:
bind(server_socket, (struct sockaddr* )&server_addr, sizeof(server_addr));
为什么bind函数第二个参数要求sockaddr结构体,却传入 sockaddr_in结构体?
因为sockaddr结构体将所有信息全部保存在一个数组中,不方便进行赋值。
3. 等待连接请求函数 listen
int listen(int sock, int backlog);
调用成功返回0,失败返回-1。
(1)sock:监听套接字的文件描述符。
(2)backlog:连接请求等待队列的长度。当客户端发起连接请求时会进入连接请求等待队列,服务端通过队列的顺序依次进行连接。
调用listen函数如下:
listen(server_socket,5);
4. 处理连接请求函数 accept
int accept(int sock, struct sockaddr* addr, socklen_t* addrlen);
调用成功返回套接字文件描述符,失败返回-1。
(1)sock:服务器套接字的文件描述符。
(2)addr:客户端地址信息的结构体变量地址。函数会自动计算连接的客户端的地址信息保存在此结构体变量中。
(3)addrlen:第二个参数长度的变量地址。
调用accept函数如下:
client_socket = accept(server_socket,(struct sockaddr*)&client_addr,&client_len);
5. 关闭套接字函数 close
int close(int sockfd);
服务端在accept后会创建一个clientsocket和客户端的clientsocket相连接,所以客户端和服务端都要close这个clientsocket。
二、客户端函数
1. 请求连接函数 connect
int connect(int sock, struct sockaddr* addr, socklen_t addrlen);
调用成功返回0,失败返回-1。
(1)sock:客户端套接字文件描述符
(2)addr:保存服务器端地址信息的结构体变量的地址。在客户端代码中,同样要进行服务端地址的初始化。
(3)addrlen:结构体变量长度。
三、完整代码
使用上述函数编写一个简单的TCP客户端和服务端,客户端发送一个字符串,服务端接收并打印,客户端输入q则断开连接。
服务端代码:
#include<iostream>
#include<string.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<unistd.h>
using namespace std;
int main()
{
char buf[1024];
int server_socket;
int client_socket;
server_socket = socket(PF_INET,SOCK_STREAM,0);
if(server_socket == -1)
cout << "socket error" << endl;
char* server_ip = "127.0.0.1";
char* server_port = "9955";
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
memset(&server_addr,0,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(server_ip);
server_addr.sin_port = htons(atoi(server_port));
if(bind(server_socket,(struct sockaddr*)&server_addr,sizeof(server_addr)) == -1)
cout << "bind error" <<endl;
if(listen(server_socket,5) == -1)
cout << "listen error" << endl;
client_socket = accept(server_socket,(struct sockaddr*)&client_addr,&client_len);
if(client_socket == -1)
cout << "accept false" << endl;
while(recv(client_socket,buf,sizeof(buf),0)) //当客户端close后,发送EOF,退出循环
{
send(client_socket,buf,sizeof(buf),0);
cout << buf << endl;
}
close(client_socket);
close(server_socket);
return 0;
}
客户端代码:
#include<iostream>
#include<string.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<unistd.h>
using namespace std;
int main()
{
int client_socket;
struct sockaddr_in server_addr;
char* server_ip = "127.0.0.1";
char* server_port = "9955";
char buf[1024];
client_socket = socket(PF_INET,SOCK_STREAM,0);
memset(&server_addr,0,sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(server_ip);
server_addr.sin_port = htons(atoi(server_port));
if(connect(client_socket,(struct sockaddr*)&server_addr,sizeof(server_addr))==-1)
cout << "connect error" << endl;
int strlen,recvlen;
while(1)
{
cout << "input: ";
cin >> buf;
if(!strcmp(buf,"q"))
break;
strlen = send(client_socket,buf,sizeof(buf),0);
recvlen = 0;
while(recvlen < strlen)
recvlen += recv(client_socket,&buf[recvlen],sizeof(buf),0); //确保数据接收完全
}
close(client_socket);
return 0;
}
四、 基于TCP的半关闭 shutdown
int shutdown(int sock, int howto);
调用成功返回0,失败返回-1。
(1)sock:套接字文件描述符
(2)howto:断开方式。SHUT_RD断开输入流、SHUT_WR断开输出流、SHUT_RDWR同时断开输入输出流。
TCP连接建立后,两端点之间会产生两条输出到输入的流,close函数会直接关闭两条流,但是shutdown函数可以选择只关闭其中一条流。比如端点发送完毕数据,则关闭输出流,向另一方发送一个EOF消息,此时不能再发数据了,但是可以继续接收另一方发来的数据。
五、 套接字可选项 getsockopt & setsockopt
上面我使用的套接字都是默认套接字,但是套接字的属性也可以修改。
读取可选项信息函数getsockopt:
int getsockopt(int sock, int level, int optname,
void* optval, socklen_t* optlen);
修改可选项信息函数setsockopt:
int setsockopt(int sock, int level, int optname,
const void* optval, socklen_t optlen);
(1)sock:套接字文件描述符
(2)level:可选项协议层。如SOL_SOCKET、IPPROTO_TCP等
(3)optname:可选项名。可选项协议层中包含的选项名
(4)optval:存储选项信息的地址
(5)optlen:optval的字节数
在此我用可选项SO_REUSEADDR举例。
我在运行服务端代码时遇到了一个问题,如果服务端非正常关闭,再次运行服务端时会报错:地址绑定错误。我查看端口状态发现端口正处于time_wait。
这种情况出现的原因是:先发送fin消息的主机在断开连接后会进入time_wait状态。这样如果另一方未收到确认终止连接的包时,可以重传确认包,保证另一方收到确认包正常终止。
但是这个time_wait状态往往会影响我们测试代码,所以可以设置套接字的可选项,让其可以绑定处于time_wait状态的端口号。
函数调用如下:
int opt = true;
socklen_t optlen = sizeof(opt);
setsockopt(server_socket,SOL_SOCKET,SO_REUSEADDR,(void* )&opt,optlen);