Linux高性能服务器编程学习记录——五、linux网络编程基础api

1、网络字节序

字节序点这里

2、socket地址结构体

TCP/IP协议族有sockaddr_in和sockaddr_in6两个专用的socket地址结构体,分别用于IPv4和IPv6。

struct sockaddr_in
{
	sa_family_t sin_family; //地址族:AF_INET
	u_int16_t sin_port; 	//端口号,要用网络字节序表示
	struct in_addr sin_addr; //IPv4地址结构体 见下面
};
struct in_addr
{
	u_int32_t s_addr; 		//IPv4地址,要用网络字节序表示
};

struct sockaddr_in6
{
	sa_family_t sin6_family; 	//地址族:AF_INET6
	u_int16_t sin6_port; 		//端口号,要用网络字节序表示
	u_int32_t sin6_flowinfo; 	//流信息,应设置为0
	struct in6_addr sin6_addr;	//IPv6地址结构体 见下面
	u_int32_t sin6_scope_id; 	//scope ID,尚处于实验阶段
};
struct in6_addr
{
	usigned char sa_addr[16]; 	//IPv6地址,要用网络字节序表示
};

3、IP地址转换函数

#include 
int inet_pton(int af, const char* src, void* dst);
const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt);

inet_pton函数将用字符串表示的IP地址src转换成用网络字节序整数表示的IP地址,并将结果存于dst指向的内存中。成功返回1,失败返回0。
af参数指定地址族,可以是AF_INET或AF_INET6。
inet_ntop进行相反的转换,前三个参数与inet_pton参数相同,最后一个cnt指定目标存储单元的大小。下面的两个宏能帮我们指定这个大小:

#include
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46

inet_ntop成功时返回目标存储单元的地址,失败返回NULL,并设置errno。

4、创建socket连接

创建一个socket,

#include 
#include 
int socket(int domain, int type, int protocol);

domain指定要使用的底层协议族,对于TCP/IP协议族而言,为PF_INET(Protocol Family of Internet)或PF_INET6,对于UNIX本地域协议族为PF_UNIX。
type指定服务类型,主要是SOCK_STREAM(流服务,TCP),SOCK_UGRAM(数据报,UDP)。自Linux内核版本2.6.17起,type参数可以接受上述服务类型与SOCK_NONBLOCK和SOCK_CLOEXEC相与的值,它们分别表示将新创建的socket设为非阻塞的,以及用fork创建子进程时在子进程中关闭该socket。
protocol表示一个具体的协议,但是通常前两个参数就唯一确定了第三个值,所以一般设为0表示使用默认协议。
socket系统调用成功返回一个socket文件描述符,失败返回-1并设置errno。

命名(绑定)socket,即将新创建的socket绑定到指定地址

#include 
#include 
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);

bind将addr指定的地址绑定到sockfd上,addrlen为sizeof(addr)。对于TCP字节流(UDP数据报)通信,socket类型是AF_INET和SOCK_STREAM(SOCK_DGRAM),socket上绑定的是IP、Port等信息,对于本地进程间通信,socket类型是AF_LOCAL和SOCK_STREAM(SOCK_DGRAM),socket上绑定的是一个本地文件,这也是本地socket和网络socket之间的最大区别。
另外AF_UNIX也属于本地socket,与AF_LOCAL是等价的。
要注意的是sockaddr是旧的地址结构,从新的结构到旧的结构直接强制转换就可以了,看后面的代码就清楚了。
bind成功返回0,失败返回-1,并设置errno。

监听socket

#include 
int listen(int sockfd, int backlog);

sockfd指定被监听的socket。(面试预警)backlog参数提示内核监听队列的最大长度,即accept队列长度。监听队列的长度如果超过backlog,服务器将不受理新的客户端连接,客户端也将收到ECONNREFUSED错误信息。在内核版本2.2前,backlog指所有处于半连接状态(SYN_RCVD)和全连接状态的socket(ESTABLISHED)的上限。2.2之后只表示全连接队列上限,半连接队列上限由/proc/sys/net/ipv4/tcp_max_syn_backlog定义,典型值是5。但是我的默认值是256。
Linux高性能服务器编程学习记录——五、linux网络编程基础api_第1张图片

接收连接

#include 
#include 
int accept(int sockfd, struct sockaddr *addr, socklen_t* addrlen);

addr用来获取被接受连接的远端socket地址,长度由addrlen指定。
accept成功时返回一个新的连接socket文件描述符,失败时返回-1,设置errno。
accept函数是阻塞的。
(面试预警)处于ESTABLISHED状态的连接在被accept前,如果出现异常,如掉线(仍处于ESTABLISHED)或提前退出(处于CLOSE_WAIT),accpet是否能正常返回。答案为是的,能正确返回,因为accpet只负责从对队列中取出连接,而不检查连接状态,更不关心网络状况的变化。
由上面的CLOSE_WAIT想到另外一个问题,即处于accept队列里的连接在收到客户端的断开请求后只是回了个ACK,却并不能发送自己的FIN,因此会一直处于CLOSE_WAIT即半关闭状态,直到它被accept后调用close或着程序退出。下面是我的测试代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char* argv[])
{
    if(argc <= 2)
    {
	printf("usage:%s ip_address port_number\n", basename(argv[0]));
	return 1;
    }

    const char* ip = argv[1];
    int port = atoi(argv[2]);

    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);

    int sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);

    int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
    assert(ret != -1);

    ret = listen(sock, 5);
    assert(ret != -1);
    
    //暂停20s以等待客户端连接和相关操作(掉线或退出)
    sleep(20);
    struct sockaddr_in client;
    socklen_t client_addrlength = sizeof(client);
    int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
    if (connfd < 0)
    {
	printf("errno is : %d\n", errno);
    }
    else
    {
	//接收连接成功 打印客户端ip和port
	char remote[INET_ADDRSTRLEN];
	printf("connected with ip: %s and port: %d \n", inet_ntop(AF_INET,
		    &client.sin_addr, remote, INET_ADDRSTRLEN), ntohs(client.sin_port));
//	close(connfd);
    }
    close(sock);
    while(1)
    {
	printf("loop...\n");
	sleep(2);
    }
    return 0;
}

下面是tcpdump输出,telnet退出后,状态变为CLOSE_WAIT,我始终没有调用close,直到最后程序退出,状态又变为LAST_ACK
在这里插入图片描述
在这里插入图片描述
Linux高性能服务器编程学习记录——五、linux网络编程基础api_第2张图片

发起连接

服务端listen之后(注意:不论是否accept了),客户端就可以调用connect发起连接。

#include 
#include 
int connect(int sockfd, const struct sockaddr* server_addr, socklen_t addrlen);

sockfd是由socket系统调用的返回值,server_addr是需要连接的服务端的socket地址,addrlen是长度。
connect成功返回一个socket,失败返回-1,并设置errno。
这里又想到一个问题:既然服务端只要listen之后,客户端就可以连接成功,那么在没有accpet的情况下,客户端发送数据 服务端是否能收到呢?验证一下!
test_server.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define  BUFFER_SIZE 512
int main(int argc, char* argv[])
{
    const char* ip = "127.0.0.1";
    int port = 12345;

    struct sockaddr_in address;
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &address.sin_addr);
    address.sin_port = htons(port);

    int sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);

    int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
    assert(ret != -1);

    ret = listen(sock, 5);
    assert(ret != -1);
    
    //暂停20s以等待客户端连接和相关操作(掉线或退出)
	printf("start sleep!\n");
    sleep(20);
	printf("wakeup!\n");
    struct sockaddr_in client;
    socklen_t client_addrlength = sizeof(client);
    int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
    if (connfd < 0)
    {
		printf("errno is : %d\n", errno);
    }
    else
    {
	//接收连接成功 打印客户端ip和port
		char remote[INET_ADDRSTRLEN];
		printf("connected with ip: %s and port: %d \n", inet_ntop(AF_INET,
			&client.sin_addr, remote, INET_ADDRSTRLEN), ntohs(client.sin_port));
		//尝试读取数据
		char buffer[BUFFER_SIZE];
		memset(buffer, '\0', BUFFER_SIZE);
		while (recv(connfd, buffer, BUFFER_SIZE-1, 0))
		{
			printf("recv from client %s\n", buffer);
		}
		close(connfd);
    }
    close(sock);
    return 0;
}

test_client.c

#include "header.h"

#define BUFFER_SIZE 10

int main(int argc, char* argv[])
{
	const char* ip ="127.0.0.1";
	int port = 12345;

	struct sockaddr_in server_address;
	bzero(&server_address, sizeof(server_address));
	server_address.sin_family = AF_INET;
	server_address.sin_port = htons(port);
	inet_pton(AF_INET, ip, &server_address.sin_addr);
	
	int sock = socket(PF_INET, SOCK_STREAM, 0);
	assert(sock >= 0);

	//主动连接服务器  并发送数据
	if (connect(sock, (struct sockaddr*)&server_address, sizeof(server_address)) != -1)
	{
		char buffer[BUFFER_SIZE];
		memset(&buffer, 'a', BUFFER_SIZE);
		send(sock, buffer, BUFFER_SIZE, 0);
		printf("client send complete!\n");
	}
	else
	{
		printf("connect error %d\n", errno);
	}
	close(sock);
	return 0;
}

下图是实验结果,注意tcpdump部分红框部分,前一个包是客户端发完数据调用close向服务端发送了FIN后,服务端回复的ACK,后一个包是3s后服务端sleep结束,accept了这条连接后,读出数据,然后调用close给客户端发送FIN。实验表明,虽未被accept,但是处于ESTABLISHED状态的连接已是一条完整的连接,它有自己的接收缓冲区,所以是可以接受发来的数据的,只是接收缓冲区的数据不能被读取而已。
Linux高性能服务器编程学习记录——五、linux网络编程基础api_第3张图片

关闭连接

关闭一个连接就是关闭连接对应的socket,通常是通过调用关闭文件描述符的系统调用close来完成。

#include 
int close(int fd);

值得注意的是close系统调用并非总是立即关闭一个连接,而是将fd的引用计数减1,只有当fd的引用计数为0,才能真正关闭连接。多进程程序中,一次fork默认将使父进程中打开的socket的引用计数加1,因此必须在父进程和紫禁城中都对该socket执行close才能将连接关闭。
如果无论如何都要立即终止连接(而不是将socket的引用计数减1),可以用shutdown,与close相比,shutdown被称为优雅的关闭连接,因为它可以通过参数来控制关闭方式,见下面,而close是关闭读写,shutdown是专门为网络编程设计的。

#include 
int shutdown(int fd, int howto);

howto参数决定了shutdown的行为:

  • SHUT_RD:关闭本侧的读,应用程序不能再针对socket文件描述符执行读操作,并且该socket接收缓冲区中的数据都被丢弃
  • SHUT_WR:关闭本侧的写,应用程序不可再对该socket文件描述符执行写操作,socket发送缓冲区中的数据会在真正关闭连接前全部发送出去。这种情况下,连接处于半关闭状态
  • SHUT_RDWR:同时关闭fd上的读和写

5、数据读写

TCP数据(流数据)读写

Linux中一切皆文件,tcp连接当然也就可以通过使用对文件的读写操作read和write来读写socket数据。另外socket编程接口提供了几个专门用于socket数据读写的系统调用,增加了对数据读写时的控制。

#include 
#include 
ssize_t recv(int sockfd, void* buf, size_t len, int flags);
ssize_t send(int sockfd, const void* buf, size_t len, int flags);

recv读取sockfd上的数据,存入buf指定的缓冲区,len是buf大小,flags参数见后面,通常设置为0。recv成功时返回读到的数据长度,返回0表示对方关闭了连接,出错返回-1,并设置errno。
send往sockfd写数据,数据来源于buf,len是buf大小,成功返回实际send的数据长度,失败返回0并设置errno。书上没说在对方关闭连接后send会返回啥(-1?),可以写代码测试下。
下图是flags参数的可选值(来自《Linux高性能服务器编程》)
Linux高性能服务器编程学习记录——五、linux网络编程基础api_第4张图片

UDP数据读写

#include 
#include 
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);
ssize_t sendto(int sockfd,const void* buf, size_t len, int flags,const struct sockaddr* dest_addr, socklen_t* addrlen);

前四个参数与TCP读写接口recv和send相同,由于udp不是面向连接的,所以src_addr用于指定通信对端的地址。
值得一提的是,recvfrom和sendto同样可以用于tcp连接的读写,只需将后面的地址置位NULL。

通用数据读写函数

可用于TCP流数据和UDP数据报

#include 
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr* msg, int flags);

下图是msghdr结构体的定义
Linux高性能服务器编程学习记录——五、linux网络编程基础api_第5张图片
msg_name成员指向一个socket地址结构变量,对于TCP,该成员没有意义,必须被设为NULL。
iovec*结构体定义如下:

iov_base指向一块内存的起始地址,iov_len是其长度。msg_iovlen指定这样的iovec结构对象有多少个。
对于recvmsg,数据将被读取并存放在msg_iovlen块分散的内存中,这些内存的位置和长度由msg_iov指向的数组指定,这成为分散读(scatter read),对于sendmsg,msg_iovlen块分散内存中的数据将被一并发送,这称为集中写(gather write)。
msg_control和msg_controllen成员用于辅助数据的传送。
msg_flags将会复制recvmsg/sendmsg中的flags,这些flags的含义与send/recv中的相同。

6、socket选项

下面两个系统调用专门用来读取和设置socket文件描述符属性:

#include 
int getsockopt(int sockfd, int level, int option_name, void* option_value, socklen_t* restrict option_len);
int setsockopt(int sockfd, int level, int option_name, const void* option_value, socklen_t option_len);

level指定要操作哪个协议的选项(即属性),如IPv4、IPv6、TCP等,option_name指定选项名字,option_value和option_len参数分别被操作选项的值和长度。两个函数都是成功时返回0,失败返回-1并设置errno。下图是socket通信中几个比较常用的socket选项。(图片来自《Linux高性能服务器编程》)
Linux高性能服务器编程学习记录——五、linux网络编程基础api_第6张图片
面试预警:SO_REUSEADDR和TCPNODELAY选项。
SO_REUSEADDR使处于TIME_WAIT状态的连接的socket地址可以被使用。
TCP_NODELAY用来禁用Nagle算法。Nagle算法规定了在任意时刻,最多只能有一个未被确认的小段。小段是指数据长度小于MSS的数据块。如现在需要发送两个包A和B,A和B都小于MSS,先发送A,在nagle算法下,在未收到A的ACK前(此时A为未被确认的小段),不能发送B。可以看出Nagle算法虽然减少了网络中包的数量,但是却带来了延迟。一些交互性强的应用(如网络游戏)不允许这种事情发生,可以通过设置TCP_NODELAY选项来禁用Nagle算法。

7、其他一些API

带外标记:sockatmark判断sockfd是否处于带外标记,即下一个被读取到的数据是否使带外数据

#include 
int sockatmart(int sockfd);

地址信息函数:getsockname和getpeername分别用于获取本端与对端socket地址

#include 
int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len);
int getpeername(int sockfd, struct sockaddr* address, socklen_t* address_len);

获取主机信息

#include 
struct hostent* gethostbyname(const char* name);
struct hostent* gethostbyaddr(const void* addr, size_t len, int type);

获取服务信息

#include 
struct servent* getservbyname(const char* name, const char* proto);
struct servent* getservbyport(int port, const char* proto);

上面获取主机的两个函数与获取服务的两个函数都是不可重入的,即非线程安全的。netdb.h里也给出了可重入的版本,在原函数名后面加上_r即可。


getaddrinfo既能通过主机名获取IP地址(内部使用gethostbyname)也能通过服务名获取端口号(内部使用getservbyname),它是否可重入取决于内部调用的版本

#include 
int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result);

getnameinfo函数能通过socket地址同时获得以字符串表示的主机名(内部使用gethostbyaddr)和服务名(内部使用getservbyport),它是否可重入叶取决于内部调用的版本

#include 
int getnameinfo(const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklen_t hostlen, char* serv, socklen_t servlen, int flags);

你可能感兴趣的:(linux,TCP-IP,c/c++,tcp/ip,网络,服务器)