`
zhuyifeng
  • 浏览: 44930 次
  • 性别: Icon_minigender_1
社区版块
存档分类
最新评论

linux网络编程手记

 
阅读更多

做linux下的网络编程有一段时间了,中间遇到过很多问题,其中不少是因为自己对网络编程和网络协议的一些基本概念搞不清楚,趁着今天没心情干活就把自己在网络编程方面的理解和一些经验总结一下,Request For Comments。

在诸多的网络协议中接触的最多也最紧密的无疑是TCP和UDP,SCTP之前因为项目原因也研究过,不过最终由于方案修改给抛弃了,TCP年代已经很久远,在网上的资料也非常多,而且我感觉它是一种非常复杂的协议,感觉要把编好基于TCP的程序光简单地了解几个socket API是不够的,刚开始接触网络编程的时候自己确实也吃了不少苦头,后来我还专门拿时间出来阅读了一下RFC,再加上长时间的实践总算也对TCP有所了解,把自己的一些经验和教训都总结一下。

首先说一下TCP的状态转移图,这个应该是很重要的,了解TCP运行周期的各种状态才能更好地运用netstat之类的应用程序去对程序进行调试,我这里收藏了一张图,是TCP的状态图,记不清是从哪里找来的,也不知道直接版权该给谁,但这张图应该最终是出自于UNP第一卷的,那copyright就是UNP了吧。

1.TCP连接状态

连接建立的几个状态没什么可说的,TCP的三次握手众所周知,更重要的是TCP连接中止的几个状态,应该可以说是连接中止需要四次握手吧。

当Client调用close函数主动关闭socket时,连接状态被标记为FIN_WAIT_1,Server在收到FIN之后read函数会返回0,这里server知道Client已经关闭连接,回复ACK,这里client连接状态被标记为FIN_WAIT_2,接下来Server调用close函数关闭连接,这时候Server向client发送FIN,Client收到之后将状态标记为TIME_WAIT,并回复ACK。

TIME_WAIT这个状态存在的意义在于Client回复的ACK未必会被Server收到,可能在传输过程中导致包的丢失,而这里Server未收到ACK之后会重新向Client发送FIN,如果client未将状态标记为TIME_WAIT而是直接标记为CLOSED,则Server发送的FIN会直接收到RST,导致Server端的发送错误,因此Client需要保证有一个TIME_WAIT状态,而这个状态会持续两位的MSL(最大段生命周期),从而保证Server成功发送FIN并发送ACK,为了保证两个数据段传输的最大时间,因此TIME_WAIT持续的时间为两倍的MSL。

Server在收到第一个FIN之后会将状态标记为CLOSE_WAIT,此时是client主动关闭连接,这里Server也需要调用Close给Client发送FIN(如上所述),之后Server的状态标记为LAST_ACK,表示Server正在等待Client发送的最后一个ACK,当Server收到最后一个ACK便会将连接标记为CLOSED,这时连接结束。TIME_WAIT这个状态和套接字的SO_REUSEADDR选项是有关系的,这个留做后面讨论。

2.TCP连接异常情况

TCP连接异常分为很多种情况,无论是客户端程序还是服务器端程序都需要考虑周全的。

Server在连接的过程中程序崩溃或者CTRL+C中止程序,或者kill接Server进程。这时会导致Server立即发送一个FIN数据包给Client,Client如果此时正在调用recv函数,则recv函数返回0,表示服务器已关闭连接,如果Client调用send函数继续向Server发送数据,Server在收到后会回复RST,而此时send方法会触发SIGPIPE信号,表示通信管道已断开,在程序中如果对该信号不做处理则会导致程序的崩溃,一般在程序开始时会忽略此信号,则在这种情况下send函数会返回-1,表示发送失败,处理SIGPIPE的代码如下:

前几天实验室这个破项目非要加上什么流媒体的功能,简单起见使用了VLC来实现,客户端这边就得需要把相关的播放界面整合到现有的界面里面来,之前的客户端UI我都是用GTK实现的,没办法,GTK用得比较多,相对熟练一些就用GTK来做了,没想到要把VLC整到GTK里面来那么麻烦,原生的libvlc是不支持GTK的,需要加一层libvlc-gtk,从网上好不容易下载到了libvlc-gtk的源码,从哪里下的也记不清了,反正就是零散地几个文件,没有README甚至连Makefile都没有,没办法首先得先写个Makefile把它编译一下,libvlc-gtk一共有八个文件,Makefile如下:

struct sigaction sa;
sa.sa_handler = SIG_IGN;
sigaction(SIGPIPE, &sa, 0 );
 

另外在这种情况下select函数也会立即返回,socket描述符会被设置,而试图从该socket中recv数据,则会返回-1。

另外一种情况是Server系统崩溃或者网络直接异常或断开,这时候Server不可能再给Client发送FIN包,而Client调用send函数后会导致数据包一直重传直接超时后返回-1,而recv函数也会一直阻塞直接超时后返回-1。这种情况就很难判断是Server端进程关闭还是网络异常,这种情况一般会用TCP的KEEP ALIVE机制,每隔一定的时间向对方发送一个只有一字节数据内容的数据包,对端收到后会返回一个ACK,以此来确保连接正常,如果未收到ACK,会尝试重传,直到重试规定次数后可以将与对端的连接标记为断开,send和recv将会返回-1。KEEP ALIVE的使用方法如下:

int tcp_keep_alive(int socketfd)
{
	int keepAlive = 1;
	int keepIdle = 10;         /* 开始发送KEEP ALIVE数据包之前经历的时间 */
	int keepInterval = 10;   /* KEEP ALIVE数据包之前间隔的时间 */
	int keepCount = 10;     /* 重试的最大次数 */
 
	if(setsockopt(socketfd , SOL_SOCKET , SO_KEEPALIVE
				,(void*)&keepAlive,sizeof(keepAlive)) == -1){
		debug_info("set SO_KEEPALIVE failed\n");
		return -1;
	}
 
	if(setsockopt(socketfd , SOL_TCP , TCP_KEEPIDLE
				,(void *)&keepIdle,sizeof(keepIdle)) == -1){
		debug_info("set TCP_KEEPIDEL failed\n");
		return -1;
	}
 
	if(setsockopt(socketfd , SOL_TCP , TCP_KEEPINTVL
				,(void *)&keepInterval,sizeof(keepInterval)) == -1){
		debug_info("set TCP_KEEPINTVL failed\n");
		return -1;
	}
 
	if(setsockopt(socketfd , SOL_TCP , TCP_KEEPCNT
				,(void *)&keepCount,sizeof(keepCount)) == -1){
		debug_info("set TCP_KEEPCNT failed\n");
		return -1;
	}
	return 1;
}
 

上面这个函数只针对Linux,昨天有网友告知在Mac OS上TCP_KEEPIDLE ,TCP_KEEPINTVL, TCP_KEEPCNT这些宏将未定义。另外对于这些参数的设置也是需要注意的,很多系统中它们的设置并不是对单个socket描述符起作用的,而是该机器上的所有socket描述符起作用的,所以这个需要注意(这个是从UNP里面看到的)。

3.关于字节顺序
Linux的主机字节顺序是采用little-endian字节顺序,而网络字节顺序是采用big-endian字节顺序,字节顺序转换是必需的。写了一个小程序来检测字节顺序,不知道对不对,Request For Comment.

#include 
 
int main(int argc, char **argv)
{
	short s = 0x0102;
	if((*(unsigned char*)&s) == 2)
		printf("little endian\n");
	else if((*(unsigned char*)&s) == 1)
		printf("big endian\n");
	else
		printf("unknown endian\n");
 
	return 0;
}
 

3.关于send和recv

写过socket程序的人肯定都会知道send和recv函数并不会总是返回要求发送或读取的字节数,如:

int ret = recv(sk, buf, 2096, 0);
 

这句话并不总是读取到完整地2096个字节,相反地,大多数情况下都不能将buf读满,recv只能返回当前可以读取到的字节数,如果协议规定本次读取肯定会读取到N个字节,那我一般的做法会写一个这样的函数来确保读取到固定的字节数:

int buf_recv(int sock, void *buf, size_t len, int flags)
{
	int n, ret;
	if(len == 0) return 0;
	for(n=0;n!=len &&(ret = recv(sock, buf+n, len-n, flags)) != -1 &&ret; n += ret);
	return (n!=len)? -1:n;
}
 

关于这两个函数还有很重要的一点是应该尽可能大地一次发送或接收更多地数据,当然前提是缓冲区中有这些数据的话,原因很简单,当通信链路很好的时候数据可能会填满系统缓冲区,而recv便是从缓冲区中读取数据,这时候一次读取更多地字节就意味着可以少调用几次recv函数,而这些函数通常都是调用了系统调用,需要进行内核态和用户态上下文的切换,也就意味着多调用几次recv会带来额外的开销,之前写的一个代理服务器的程序数据传输速度一直很低,后来修改了recv和send的缓冲区大小后速率提高了近一倍。

4.关于非阻塞模式

一般应用的时候都是使用阻塞式IO,至少我在大多数情况下都用的阻塞式IO,非阻塞很少应用,但存在便我价值,我用到的非阻塞IO的情况一般是用来进行超时connect,首先将socket设为非阻塞模式,connect立即返回-1,此时已向对端发送FIN,而并未来得及收到任何ACK,于是直接返回-1,但并不代表连接失败,errno会被置为EINPROGRESS ,表示连接正在进行中,然后通过select来设置socket可写的超时时间,如果规定时间内可写,且socket并无出错,则表示连接成功,socket出错则表示连接失败,或规定时间内不可写则表示连接超时,简单地写了如下代码:

#include
#include
#include
#include
#include
#include
#include 
 
int main(int argc, char *argv[])
{
	int                sk;
	int                flags;
	int                err = 0;
	int                ret;
	socklen_t          len;
	struct sockaddr_in addr;
	fd_set             fd_write;
	struct timeval     tv;
 
	if( (sk = socket(AF_INET, SOCK_STREAM, 0)) == -1 ) {
		perror("socket");
		return 1;
	}
 
	if( (flags = fcntl(sk, F_GETFL, 0)) == -1 ){
		perror("fcntl GET flags failed");
		return 1;
	}
 
	if(fcntl(sk, F_SETFL, flags | O_NONBLOCK) == -1) {
		perror("fcntl SET flags failed");
		return 1;
	}
 
	memset(&addr, 0, sizeof(addr));
	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = inet_addr("59.64.129.169");
	addr.sin_port = htons(808);
 
	if(connect(sk, (struct sockaddr*)&addr, sizeof(addr)) == -1	) {
		if(errno != EINPROGRESS) {
			perror("connect");
			return 1;
		}
		FD_ZERO(&fd_write);
		FD_SET(sk, &fd_write);
		tv.tv_sec = 5;
		tv.tv_usec = 0;
 
		ret = select(sk + 1, (fd_set*)0, &fd_write, (fd_set*)0, &tv);
 
		if(ret > 0){
 
			if(FD_ISSET(sk, &fd_write)) {
				len = sizeof(int);
 
				if(getsockopt(sk, SOL_SOCKET, SO_ERROR, &err, &len) == 0) {
					if(err == 0) { printf("connect success\n"); return 0; }
					else { fprintf(stderr, "connect:%s\n", strerror(err)); return 1; }
				}else{
					fprintf(stderr, "getsockopt:%s\n", strerror(err));
					return 1;
				}
			}else{
				fprintf(stderr, "connect(FD_ISSET) failed\n");
				return 1;
			}
 
		}else if(ret == 0) {
			fprintf(stderr, "connect timeout\n");
			return 1;
		}else {
			fprintf(stderr, "connect(select):%s\n", strerror(errno));
			return 1;
		}
	}else{
		fprintf(stderr, "connect:%s\n", strerror(errno));
		return 1;
	}
	return 0;
}
 

5.关于select多路复用

select是网络编程中很常用的函数,用来进行IO多路复用,但之前我一直忽略了一个问题,当select返回时会将本次检查中不可用的描述符(如不可读或不可写)的描述符从描述符集中删除,只保留当前可用的描述符,在对多个socketfd进行利用的时候需要注意,每次循环select之前都需要在select之前用FD_SET重新设置描述符,否则之后便只能返回第一次可读的描述符了。

6.关于UDP广播

UDP广播这个也简单说一下,首先255.255.255.255这个地址是不能被路由的,只能被本物理网络的数据包接收。UDP广播之前需要给socket设置SO_BROADCAST选项:

int brodopt = 1;
setsockopt(cp_usock, SOL_SOCKET, SO_BROADCAST, &brodopt, sizeof(brodopt));
 

UDP广播需要注意的一点是广播接收时接收端bind的本地地址的问题,接收端必须绑定INADDR_ANY这个地址才可以接收广播包,如果是绑定的某个特定的地址则无法接收广播包。

OK,简单说这么几条,都是我编程时候遇到的经验总结,以后再遇到什么问题再接着补充。

原创文章,转载请注明: 转载自basic coder

本文链接地址: http://basiccoder.com/linux-network-programing-note.html
 
分享到:
评论

相关推荐

    linux网络编程手记.doc

    Linux网络编程是一个深入且复杂的话题,涉及到操作系统内核、网络协议栈以及应用程序接口等多个层面。在TCP/IP协议族中,TCP(Transmission Control Protocol)和UDP(User Datagram Protocol)是最常用的两种传输层...

    linux 编程手记源码

    【标题】"Linux编程手记源码"涉及的是在Linux环境下进行C语言编程的相关实践,主要涵盖的是构建和清理编译过程。这份手记可能是某位开发者在学习或开发过程中留下的笔记,其中包含了编译命令和简单的Makefile示例。 ...

    Android开发手记一_NDK编程实例

    ### Android开发手记一_NDK编程实例 #### 一、开发环境的搭建 在开始具体的NDK编程之前,首先需要确保开发环境已经被正确地搭建起来。对于初次接触Android NDK开发的朋友来说,拥有一个良好的环境配置是至关重要的...

    匠人手记(包括所有的资料很全)

    其完整性意味着覆盖了多个 IT 领域,从编程语言到系统管理,从网络技术到数据库应用,都可能包含在内。 在没有具体的文件内容预览的情况下,我们可以根据一般 IT 资料库的构成来推测可能包含的知识点: 1. **编程...

    Linux中文手册

    6.Turbo Linux 简体中文版安装手记 7.Apache+php3+PostgreSQL 8.XWindow显卡配置通用解决方法 9.Linux中的字型(FONTS)设定 10.Linux部分命令简介 11.Lilo.conf (LILO 配置文件) 手册 12.设置和修改 X Window ...

    ZedBoard学习手记

    3. **Linux系统配置与编程**:ZedBoard通常会预装Linux系统,学习过程中需要掌握如何配置Linux环境,包括网络设置、软件安装以及交叉编译等。同时,学习使用ARM Cortex-A9进行程序开发,如C/C++编程,理解进程管理、...

    x86汇编语言学习手记

    通过这样的学习手记,读者可以逐步掌握X86汇编语言的基本概念,理解C语言编译后生成的机器代码,以及如何在Unix/Linux环境下进行程序调试。这些知识对于操作系统学习、汇编手册研究以及对计算机系统底层工作原理的...

    AVR-GCC学习手记.rar_avr_mcu

    3. **AVR-GCC的安装与配置**:手记中可能包括了在不同操作系统(如Windows、Linux、Mac OS)上安装和配置AVR-GCC工具链的步骤,包括AVR-GCC、AVR-Libc、AVRDUDE等工具。 4. **基本编程环境搭建**:可能涉及如何使用...

    X86汇编语言学习手记

    ### X86汇编语言学习手记 #### 一、编译环境介绍 在学习X86汇编语言的过程中,作者Badcoffee所使用的编译环境为Solaris 9 X86操作系统,编译器选用的是gcc 3.3.2版本,链接器则使用了Solaris Link Editor S5.x版本...

    PHP Web应用开发入门体验手记.doc

    开发环境方面,PHP开发者通常会使用LAMP/WAMP集成环境,如Linux/Windows上的Apache、MySQL和PHP。对于源代码的错误检查和调试,专业IDE如Zend Studio或PhpStorm提供了强大的支持。学习PHP,通常会经历搭建开发环境、...

    glpi-ocs安装手记.docx

    总的来说,GLPI和OCS的集成安装是一个涉及多步骤、多组件的过程,需要对Linux系统、LAMP环境和Perl编程有一定程度的了解。但一旦完成,这个强大的组合将为IT资产管理提供极大的便利,能够实时监控和管理组织的硬件和...

    操作系统进程内存手记

    **进程查看**:在Linux系统中,我们可以使用`ps`命令查看当前进程状态,`top`或`htop`实时监控内存和CPU使用情况,`pmap`显示进程的内存映射。 了解这些基础知识后,你将能够更好地理解和优化你的C语言程序,有效...

    网络操作系统重难点.pdf

    - **Winsock接口**:Windows下的网络编程接口,提供了类似UNIX套接字的功能。 6. **网络管理与服务**: - **网络管理**:涉及配置、性能监控、故障检测和恢复、安全性等五个主要管理功能。 - **DNS域名解析**:...

    AVR-GCC 学习手记

    它基于GNU Compiler Collection(GCC),支持多种编程语言,如C、C++、汇编语言等。由于AVR系列微控制器广泛应用于嵌入式系统开发领域,因此AVR-GCC成为了许多开发者首选的工具之一。 ### 特点与优势 1. **开源...

    Ubuntu liunx

    3. Ubuntu手记:压缩包中的"Ubuntu 使用手记.txt"可能包含了作者在使用Ubuntu过程中的经验和技巧,是很好的学习材料。 六、进阶学习 1. shell脚本编程:学习bash语言,编写自动化任务脚本。 2. 系统管理:理解权限...

    zynq/zedboard/xlinx 学习例程及笔记

    "学习笔记"部分可能包括了对Zynq架构的详细解析,如何配置和编程Zynq的硬件部分,以及如何在Linux环境下开发应用程序。对于新手来说,理解Zynq的双处理核心架构非常重要,一个用于处理实时和并行任务,另一个则负责...

    php程序员菜鸟成长手记 php入门教程 pdf

    ### PHP程序员菜鸟成长手记 —— PHP入门教程 #### 一、PHP简介 **1. Web程序工作原理** Web程序工作原理是指用户通过浏览器发送请求到服务器,服务器处理请求后返回响应的过程。在这个过程中,PHP作为一种服务器...

    php程序员菜鸟成长手记——php入门教程

    - 跨平台:支持多种操作系统,如Windows、Linux等。 - 易于学习:语法简单,适合初学者快速上手。 - 功能强大:内置丰富的函数库,支持多种数据库接口,可以轻松实现复杂的网站功能。 - 高效性:运行速度快,...

    Typora-linux-x64-1.0.2.tar.gz

    标题中的"Typora-linux-x64-1.0.2.tar.gz"是一个针对Linux操作系统的64位版本的Typora编辑器的压缩包文件。 Typora是一款流行且用户友好的Markdown编辑器,它以其简洁的界面和实时预览功能而闻名。在Linux环境下,它...

Global site tag (gtag.js) - Google Analytics