Linux 高性能服务器网络编程(一)

本文深入探讨Linux网络编程基础,涵盖Socket地址API、IP地址转换、socket创建与管理、连接控制及读写操作等核心概念。通过详细解析C/S程序实例,帮助读者掌握网络编程技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

参考自《高性能服务器编程》,主要用于学习网络编程模块

Linux网络编程基础API

Socket 地址API

(1)网络字节序:大端字节序(低位存放高地址)
(2)主机字节序:小端字节序 (低位存放低地址)

通用socket 地址

#include <bits/socket.h>

struct sockaddr
{
	sa_family_t sa_family;
	char sa_data[14];
}

sa_family 地址族一般与协议族相对应

协议族地址族描述
PF_UNIXAF_UNIXUNIX本地域协议族
PF_INETAF_INETTCP/IPv4协议族
PF_INET6AF_INET6TCP/IPv6协议族

专用Sokect地址

注:此处只学习IPV4/IPV6协议族,对UNIX本地族域不做解释
// 這些都不建議使用!請使用 inet_pton() 或 inet_ntop() 取代!

#include <sys/un.h>
struct sockaddr_in
{
	sa_family_t sin_family;	/*地址族: AF_UNIX*/
	u_int16_t sin_port;	/*端口号, 要用网络字节序*/
	struct  in_addr sin_addr;	/*IPV4地址结构体*/
};
struct  in_addr	/*IPV4地址结构体*/
{
	u_int32_t s_addr;		
};

struct  sockaddr_in6
{
	sa_family_t sin6_family;	/*地址族: AF_UNIX*/
	u_int16_t sin6_port;	/*端口号, 要用网络字节序*/
	u_int32_t sin6_flowinfo;	/*流信息,应该设置0*/
	struct  in6_addr sin_addr;	/*IPV4地址结构体*/
	u_int32_t sin6_scope_id; 
};

struct  in_addr	/*IPV4地址结构体*/
{
	unsigned char sa_addr[16]	
};

IP地址转换函数

#include <arpa/inet.h>
/*description :点分十进制字符串表示的IPV4转换成网络字节序整数表示的IPv4地址
*return 成功 :网络字节序整数表示的IPv4地址 失败 :INADDR_NONE
*@pram strptr : 点分十进制字符串表示的IPV4
*/
in_addr_t inet_addr( const char *strptr);

/*description :点分十进制字符串表示的IPV4转换成网络字节序整数表示的IPv4地址
*return 成功 :1 失败 :0
*@pram cp : 点分十进制字符串表示的IPV4,inp:转换结果存于网络字节序结果
*/
int inet_aton( const char *cp, struct in_addr *inp);
/*description :网络字节序整数表示的IPv4地址转换成点分十进制字符串表示的IPV4
*return 成功 :成点分十进制字符串表示的IPV4 失败 NULL
*@pram in : 点分十进制字符串表示的IPV4
*/
注:此函数是不可重入函数,返回的值指向该静态内存
char *inet_ntoa( struct in_addr in);

*重点

#include <arpa/inet.h>
/*description :网络字节序转换为主机字节序
*return 成功 1 失败 0 并且设置 errno
*@pram af : AF_INET或者AF_INET6 src : 点分十进制字符串表示的IPV4或者点分十六进制字符串表示的IPV6 
*       dst : 转换结果存于网络字节序结果, size:INET_ADDRSTRLEN(16)/INET6_ADDRSTRLEN(46)
*/
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
/*description :主机字节序转换成网络字节序
*return 成功 1 失败 0 并且设置 errno
*@pram af : AF_INET或者AF_INET6 src : 点分十进制字符串表示的IPV4或者点分十六进制字符串表示的IPV6 dst : 转换结果存于网络字节序结果
*/
int inet_pton(int af, const char *src, void *dst);

创建socket(socket)

#include <sys/types.h>
#include <sys/socket.h>
/*description :创建socket文件
*return 成功:socket文件描述符 失败: 返回-1并且设置errno
*@pram domain(底层协议) : PF_INET / PF_INET6; 
*type(服务类型) :SOCK_STREAM(TCP流服务)/ SOCK_DGRAM(UDP数据报) / SOCK_NONBLOCK(设置非阻塞) / SOCK_CLOEXEC(用fork创建子进程时在子进程中关闭该socket)
*protocol : 0
*/
int socket(int domain, int type, int protocol);

命名(绑定)socket(bind)

用于绑定具体的socket和port

#include <sys/types.h>
#include <sys/socket.h>
/*description :绑定socket,将my_addr所指的socket地址分配给为命名的sockfd 文件描述符,addrlen参数指出该socket地址的长度
*return 成功:0 失败: 返回-1并且设置errno 
*		常见errno:	EACCES : 普通用户绑定到0~1023端口上/EADDREINUSE:TIME_WAIT状态的socket地址
*@pram sockfd 文件描述符; my_addr : sockaddr; addrlen : 地址长度
*/
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);

监听socket(listen)

/*description :监听指定socket文件描述符
*return 成功:0 失败: 返回-1并且设置errno 
*@pram sockfd 文件描述符;
*backlog : 指定所有处于半连接状态(SYN_RCVD)和完全连接状态(ESTABLISHED)的socket的上限
*内核2.23之后,backlog只表示完全连接状态(ESTABLISHED)的上限,而半连接直接由/proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义
*backlog参数典型值为 5
*/
int listen(int sockfd, int backlog);

注:完整连接一般为(backlog + 1),不同的系统略有不同不过监听队列中的完整的连接的上限比bakclog略大

接收连接accpet

#include <sys/types.h>
#include <sys/socket.h>
/*description :从listen监听队列中接收一个连接
*return 成功:0 失败: 返回-1并且设置errno 
*@pram sockfd : listen Socket文件描述符;addr : 获取接收连接远端的socket地址 addrlen:长度
*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

注:accpet函数只是往监听队列取出连接,不管连接处于什么状态,更加不关心网络变化

发起连接(connect)

#include <sys/types.h>
#include <sys/socket.h>
/*description :客户端主动向服务器建立连接
*return 成功:0 失败: 返回-1并且设置errno
*ECONNREFUSED 目的端口不存在	ETIMEDOUT : 连接超时 
*@pram sockfd : 返回与服务端通信的socket;serv_addr : 服务端地址 addrlen:长度
*/
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

关闭连接

#include <unistd.h>
int close( int fd);

注:并非总是关闭一个连接,而是将fd 的引用计数减1,只有当fd的引用计数为0时才真正的关闭连接,
	多进程程序中一次fork会将父进程中打开的socket + 1,因此在父子进程中都要对该socket
	执行close才能将连接关闭
/*description :关闭一方连接
*return 成功:0 失败: 返回-1并且设置errno
*@pram sockfd : 需要关闭的socket
*how :如下有详解
*/
int shutdown(int sockfd, int how)

how参数可选值

可选值含义
SHUT_RD(0)关闭sockfd的读操作,应用程序不能从sockfd执行读操作,并且把该socket接收缓冲区的数据丢弃
SHUT_WR (1)关闭sockfd的写操作,sockfd的发送缓冲区中的数据会在真正的关闭连接之前全部发出去,应用程序不在对sock执行写操作。这种情况连接处于半关闭状态
SHUT_RDWR(2)关闭sockfd的读写操作

读写操作

#include <sys/types.h>
#include <sys/socket.h>
/*description :从sockfd写数据
*return :实际读取的长度,可能小于期望值需要多次调用recv;出错时返回-1并且设置errno
*@pram buf : 读缓冲区的位置 len:读缓冲区的大小
*/
size_t send(int sockfd, const void *buf, size_t len, int flags);

/*description :从sockfd接收数据
*return :实际读取的长度,可能小于期望值需要多次调用recv;当返回 0 时对方关闭了连接, 出错时返回-1并且设置errno
*@pram buf : 读缓冲区的位置 len:读缓冲区的大小
*/
size_t recv(int sockfd, void *buf, size_t len, int flags);

在这里插入图片描述

一个基本的C/S程序

client

#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <linux/in.h>
#include <signal.h>

extern void sig_proccess(int signo);
extern void sig_pipe(int signo);
static int s;
void sig_proccess_client(int signo)
{
	printf("Catch a exit signal\n");
	close(s);
	exit(0);	
}

#define PORT 8888	/* 侦听端口地址 */
int main(int argc, char *argv[])
{

	struct sockaddr_in server_addr;	/* 服务器地址结构 */
	int err;/* 返回值 */
		
	signal(SIGINT, sig_proccess);
	signal(SIGPIPE, sig_pipe);
	
	/* 建立一个流式套接字 */
	s = socket(AF_INET, SOCK_STREAM, 0);
	if(s < 0){/* 出错 */
		printf("socket error\n");
		return -1;	
	}	
	
	/* 设置服务器地址 */
	bzero(&server_addr, sizeof(server_addr));		/* 清0 */
	server_addr.sin_family = AF_INET;				/* 协议族 */
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);/* 本地地址 */
	server_addr.sin_port = htons(PORT);				/* 服务器端口 */
	
	/* 将用户输入的字符串类型的IP地址转为整型 */
	inet_pton(AF_INET, argv[1], &server_addr.sin_addr);	
	/* 连接服务器 */
	connect(s, (struct sockaddr*)&server_addr, sizeof(struct sockaddr));
	process_conn_client(s);	/* 客户端处理过程 */
	close(s);	/* 关闭连接 */
}

server:

#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <linux/in.h>
#include <signal.h>

extern void sig_proccess(int signo);

#define PORT 8888		/* 侦听端口地址 */
#define BACKLOG 2		/* 侦听队列长度 */
int main(int argc, char *argv[])
{
	int ss,sc;		/* ss为服务器的socket描述符,sc为客户端的socket描述符 */
	struct sockaddr_in server_addr; /* 服务器地址结构 */
	struct sockaddr_in client_addr;	/* 客户端地址结构 */
	int err;	/* 返回值 */
	pid_t pid;	/* 分叉的进行id */
	
	signal(SIGINT, sig_proccess);
	signal(SIGPIPE, sig_proccess);
	
	
	/* 建立一个流式套接字 */
	ss = socket(AF_INET, SOCK_STREAM, 0);
	if(ss < 0){/* 出错 */
		printf("socket error\n");
		return -1;	
	}	
	
	/* 设置服务器地址 */
	bzero(&server_addr, sizeof(server_addr));	/* 清0 */
	server_addr.sin_family = AF_INET;			/* 协议族 */
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);/* 本地地址 */
	server_addr.sin_port = htons(PORT);			/* 服务器端口 */
	
	/* 绑定地址结构到套接字描述符 */
	err = bind(ss, (struct sockaddr*)&server_addr, sizeof(server_addr));
	if(err < 0){/* 出错 */
		printf("bind error\n");
		return -1;	
	}
	
	/* 设置侦听 */
	err = listen(ss, BACKLOG);
	if(err < 0){/* 出错 */
		printf("listen error\n");
		return -1;	
	}
	
	/* 主循环过程 */
	for(;;)	{
		int addrlen = sizeof(struct sockaddr);
		/* 接收客户端连接 */
		sc = accept(ss, (struct sockaddr*)&client_addr, &addrlen);
		if(sc < 0){		/* 出错 */
			continue;	/* 结束本次循环 */
		}	
		
		/* 建立一个新的进程处理到来的连接 */
		pid = fork();		/* 分叉进程 */
		if( pid == 0 ){		/* 子进程中 */
			close(ss);		/* 在子进程中关闭服务器的侦听 */
			process_conn_server(sc);/* 处理连接 */
		}else{
			close(sc);		/* 在父进程中关闭客户端的连接 */
		}
	}
}

地址信息函数

#include <sys/unistd.h>

/*description :获取hostName
*return :成功:0 失败: 返回-1并且设置errno
*@pram name:存储hostname len:缓冲区大小
*/
int gethostname(char *name, size_t len);
#include <sys/socket.h>
//获取本端的sock地址,将其存储在address参数指定内存中,socket长度存储在address——len参数指定变量中
//*return :成功:0 失败: 返回-1并且设置errno
int getsockname(int sockfd, struct sockaddr *address, socklen_t *address_len);
//获取本端的sock地址,将其存储在address参数指定内存中,socket长度存储在address——len参数指定变量中
//*return :成功:0 失败: 返回-1并且设置errno
int getpreename(int sockfd, struct sockaddr *address, socklen_t *address_len);

SOCKET选项(*)

#include <sys/types.h>
#include <sys/socket.h>

int getsockopt(int sockfd, int level, int optname, void *optval,
               socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval,
               socklen_t optlen);

注:服务端对listen socket设置,accpet返回后会继承这些opt选项
客户端对connect设置,因为对于客户端connect结束就已经建立了连接

在这里插入图片描述

SO_REUSEADDR : 即使sock处于TIME_WAIT状态,只要设置了SO_RCVBUF也能立即重用
也可以修改内核参数:/proc/sys/net/ipv4/tcp_rw_recycle,快速回收关闭socket,从而tcp连接不进入TIME_WAIT的状态

SO_RECVBUF/SO_SNDBUF:用setsockopt设置时,系统一般会将设置的值加倍,但是需要在一定范围内,查看 /proc/sys/net/ipv4/tcp_wmem 和 /proc/sys/net/ipv4/tcp_rmem,目的是为了确保TCP连接有足够的空闲缓冲区来处理拥塞

SO_RCVLOWAT 和SO_SNDLOWAT选项

作用:被IO复用系统调用(epoll)用来判断socket是否可读可写,当读缓冲区中可读数据大于读低水位时,到对应的socket读取数据;当写缓冲区中可读数据大于写低水位时,到对应的socket写入数据

SO_LINGER
作用:用于控制clsoe系统调用在关闭TCP连接时的行为。默认情况下,close调用之后立即将TCP发送缓冲区的数据发送给对端

#include <sys/socket.h>
struct linger
{
	int l_onoff  /*开启(非0)关闭(0)*/
	int l_linger /*滞留时间*/
};

设置不同的变量在close之后会产生三种不同的行为:
(1)l_onoff = 0,默认关闭socket
(2)l_onoff != 0,调用close立即返回,丢弃发送缓冲区的数据,发送一个RST报文,因此这是一个终止异常连接的方法
(3)l_onoff != 0, l_linger > 0;close行为取决与缓冲区是否有数据,socket是否阻塞
对于阻塞socket,close等待一段时间,直到缓冲区数据发送并且确认,如果这段时间TCP模块没有发送完数据并且得到对方确认那么close系统调用返回-1,errno = EWOWLDBLOCK

对于非阻塞,close立即返回,根据返回值和errno的值来判断数据是否发送完毕

注:

#include <netdb.h>
//查看error的字符串形式
const char *gai_strerror( int error);

高级IO函数

pipe()

创建普通管道用于进程之间的通信

#include <unistd.h>
/*description :创建一个管道,实现进程间的通信
*return :成功:0 失败: 返回-1并且设置errno
*@pram name:存储hostname len:缓冲区大小
*/
int pipe( int fd[2]);

socket基础api提供了快速创建双向管道的

#include <sys/types.h>
#include <sys/socket.h>
/*description :创建一个管道,实现进程间的通信
*return :成功:0 失败: 返回-1并且设置errno
*@pram 与socket()参数相同,不过domain 只能用AF_UNIX
*/

int socketpair(int domain, int type, int protocol, int fd[2])

dup() / dup2()

#include <unistd.h>
/*description :创建一个新的文件描述符,指向file_descriptor相同的文件,管道或者网络连接
*return :成功:0 失败: 返回-1并且设置errno , 返回值总是取系统当前最小的文件描述符
*@pram 
*/
int dup( int file_descriptor);

/*description :创建一个新的文件描述符,指向file_descriptor相同的文件,管道或者网络连接
*return :成功:0 失败: 返回-1并且设置errno , 返回值总是取不小于file_descriptor_two
*@pram 
*/
int dup( int file_descriptor, int file_descriptor_two)

注:提供dup和dup2产生的新描述符并不继承原来文件描述符的属性,比如close-on-exec 和non-blocking
同时也是CGI服务器的基础

readv() / writev()

#include <sys/uio.h>
/*description :将数据从文件描述符读到分散的内存块即分散读/将分散内存的数据写入文件描述符,相当于简化的recvmsg/sendmsg
*return :成功:返回读出/写入的fd字节数 失败: 返回-1并且设置errno , 返回值总是取不小于file_descriptor_two
*@pram struct iovec
*{
*	 // Starting address (内存起始地址)
*       void  *iov_base;   
*     // Number of bytes to transfer(这块内存长度) 
*       size_t iov_len;    
*}
*/
ssize_t readv( int fd, const struct iovec *vector, int count)
ssize_t writev( int fd, const struct iovec *vector, int count)

sendfile()

在两个文件描述符之间直接传递数据(有内核操作), 避免了内核与用户缓冲区的数据拷贝,sendfile( ) 系统调用利用 DMA 将文件中的数据拷贝到操作系统内核缓冲区中,然后数据被拷贝到与 socket 相关的内核缓冲区中。接下来,DMA 将数据从内核 socket 缓冲区中拷贝到网卡中去。如果在用户调用 sendfile ( ) 系统调用进行数据传输的过程中有其他进程截断了该文件,那么 sendfile ( ) 系统调用会简单地返回给用户应用程序中断前所传输的字节数,errno 会被设置为 success。

#include <sys/sendfile.h>
/*description :文件描述符之间传递数据
*return :成功:返回参数成功的字节数 失败: 返回-1并且设置errno , 返回值总是取系统当前最小的文件描述符
*@pram out_fd : 待写入内容,必须是个socket
*		in_fd : 待读入内容,必须指向一个文件(支持mmap函数的文件描述符),不能是socket
*		offset : 指定in_fd的那个位置开始读
*		count : 参数的字节数
*/
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

mmap() / munmap()

用于申请一段共享内存(虚拟内存),可以作为IPC 的share memory 也可以文件之间映射到内存
#include <sys/mman.h>

/*description :申请一段内存空间
* return : 成功:返回指向目标区域的指针 失败:返回MAP_FAILED((void *)-1),并且设置errno
*@pram start : 指定内存的起始地址	length : 内存长度	
*	prot : 内存段的访问权限,如下
*	PROT_READ : 内存端可读 PROT_WRITE : 内存段可写
*	PROT_EXEC : 内存段可执行	PROT_NONE :内存段不可访问
*/
void *mmap( void *start, size_t length, int prot, int flags, int fd, off_t off_t);
/*description :释放一段内存空间
*return :成功:返回参数成功的字节数 失败: 返回-1并且设置errno
*@pram start : 指定内存的起始地址	length : 内存长度	
*/
int munmap( void *start, size_t length);

flag参数:在这里插入图片描述

splice()

用于两个文件描述符之间数据移动,也是零拷贝技术

#include <fcntl.h>
ssize_t splice(int fdin, loff_t *offin, int fdout, loff_t *offout, size_t len, unsigned int flags);

参数意义:

fdin参数:待读取数据的文件描述符。
offin参数:指示从输入数据的何处开始读取,为NULL表示从当前位置。如果fdin是一个管道描述符,则offin必须为NULL。
fdout参数:待写入数据的文件描述符。
offout参数:同offin,不过用于输出数据。
len参数:指定移动数据的长度。
flags参数:表示控制数据如何移动,可以为以下值的按位或:

SPLICE_F_MOVE:按整页内存移动数据,存在bug,自内核2.6.21后,实际上没有效果。
SPLICE_F_NONBLOCK:非阻塞splice操作,实际会受文件描述符本身阻塞状态影响。
SPLICE_F_MORE:提示内核:后续splice将调用更多数据。
SPLICE_F_GIFT:对splice没有效果。
fdin和fdout必须至少有一个是管道文件描述符。

返回值:

返回值>0:表示移动的字节数。
返回0:表示没有数据可以移动,如果从管道中读,表示管道中没有被写入数据。
返回-1;表示失败,并设置errno。

errno值如下:

EBADF:描述符有错。
EINVAL:目标文件不支持splice,或者目标文件以追加方式打开,或者两个文件描述符都不是管道描述符。
ENOMEM:内存不够。
ESPIPE:某个参数是管道描述符,但其偏移不是NULL。

fcntl() 重点

提供对文件描述符的各种控制操作,另外一个类似的系统调用ioctl,而且ioctl比fcntl执行的范围更加广

#include <fcntl.h>
int fcntl(int fd, int cmd, ...);

fd:被操作的文件描述符

cmd及其第三个定义如下图在这里插入图片描述

一般网络编程需要将文件描述符设为非阻塞的

int setnonblocking( int fd )
{
    int old_option = fcntl( fd, F_GETFL );
    int new_option = old_option | O_NONBLOCK;
    fcntl( fd, F_SETFL, new_option );
    return old_option;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值