最简单的linux tcp网络编程
2013-10-11 20:26
525 查看
本文基于linux 介绍tcp c 网络编程,很多著作在介绍linux网络编程的时候都是将网络通讯流程和纠错函数,套接字封装函数一起介绍,看得时候很充实,看完了很迷茫,特别是如果读英文原版的unix网络编程,花了很多时间,最后却一头雾水,连最基本的通讯原理都搞不清楚,如果你连通讯原理都搞不清楚,那你编程的时候肯定只能翻着参考书,将一段一段书中介绍的源代码组合在一起,那么最后你编译出来的程序如果出现bug,你自己都不知道怎么去修改。
在这里我不再介绍OSI七层网络模型,或者TCP/IP四层协议栈,随着你不断的学习使用各种网络协议的通信程序的C语言编程,你会对他们非常熟悉的。
下图是基于TCP协议的客户端/服务器程序的一般流程
图解:
建立连接的过程:
服务器调用socket(),bind(),listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,而客户端调用socket()初始化以后,调用connect()发出SYN并等待服务器应答,服务器应答一个SYN-ACK,客户端收到后从connect返回,同时应答一个ACK,服务器收到后从accept返回。
数据传输的过程:
建立连接后,TCP协议提供全双工通信服务,一般情况下是由客户端主动发起请求,服务器被动处理请求,交互方式,因此,服务器从accept返回后立刻调用read()函数,读取socket,如果没有数据到达就阻塞等待,这时客户端调用write()函数发送请求给服务器,服务器收到后从read()函数返回,对客户端的请求进行处理,在此期间客户端调用read()函数阻塞等待服务器的应答,服务器调用write()函数将处理结果发回客户端,再次调用read()函数等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环。
关闭连接过程:
如果客户端没有更多请求了,就调用close()关闭连接,发送FIN信号到服务器端,服务器的read()函数返回0,这样服务器端就知道了客户端关闭了连接,也调用close()函数关闭连接,注意,这时服务器也发送一个FIN信号给客户端,很多著作和博客上说任何一方调用close()关闭连接后,连接的两个传输方向就都关闭了,不能再发送数据了,这时不准确的,关于这一点,在下一篇博客《linux简单的TCP C交互编程》一文中将介绍到。
我这里先写一个客户端和服务器端只进行一次性数据传输就各自关闭连接的例子,只是为了讲解上面的一般流程图,读者可以完全按照图中的步骤和程序一步一步对应起来,专注于连接过程。
源代码:
tcpcli.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define MAXLINE 80
#define SERV_PORT 9877
int main(int argc,char *argv[])
{
int num,sockfd;
struct sockaddr_in servaddr;
const char sendline[MAXLINE]="hello world";
char recvline[MAXLINE];
if(argc!=2)
{
printf("usage:tcpcli <IPaddress>");
}
sockfd=socket(AF_INET,SOCK_STREAM,0);
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(SERV_PORT);
bc39
inet_pton(AF_INET,argv[1],&servaddr.sin_addr);
connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
write(sockfd,sendline,MAXLINE);
read(sockfd,recvline,MAXLINE);
printf("received from serve %s\n",recvline);
close(sockfd);
}
tcpserv.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAXLINE 80
#define SERV_PORT 9877
int main(int argc,char *argv[])
{
int listenfd,connfd;
struct sockaddr_in servaddr,cliaddr;
socklen_t clilen;
char recvline[MAXLINE];
listenfd=socket(AF_INET,SOCK_STREAM,0);
servaddr.sin_family=AF_INET;
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
servaddr.sin_port=htons(SERV_PORT);
bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
listen(listenfd,20);
connfd=accept(listenfd,(struct sockaddr *)&cliaddr,&clilen);
read(connfd,recvline,MAXLINE);
printf("received from client %s\n",recvline);
write(connfd,recvline,MAXLINE);
close(connfd);
}
我这里的所有源文件都使用g++编译器进行编译,因为g++编译器比gcc编译器在语法检查方面更加严格。
可以看到,服务器端再接收到客户端传来的数据后将其原封不动的传递回了客户端,然后客户端和服务器端分别退出。
[wind@windyang interview]$ ./tcpcli 127.0.0.1
received from serve hello world
[wind@windyang interview]$ ./tcpserv
received from client hello world
当然,再测试的过程中需要先启动服务器,再启动客户端,因为是客户端主动发起连接的。
下面我们从五个方面介绍一下在两段源代码中用到的技术和需要注意的地方:
第一:套接字地址结构体
我们要研究linux网络编程,首先要对其定义的socket进行,而对socket进行研究,就首先要研究套接字地址结构
struct in_addr
{
in_addr_t s_addr;//32位ipv4地址,网络字节序
};
struct sockaddr_in
{
uint8_t sin_len;//本结构体的长度
sa_family_t sin_family;//AF_INET
in_port_t sin_port;//16bit TCP或者UDP端口号
struct in_addr sin_addr;//32bit IPV4地址,网络字节序
char sin_zero[8];
};
可能大家看到这些例如sa_family_t等的数据类型,会感到非常陌生,我下面给出一张表格来解释一下这些类型的定义:
POSIX只需要我们定义这个结构体中的三个数数据类型:sin_family,sin_port,sin_addr.
32bit的IPv4地址可以使用两种不同的方式进行操作:例如,如果serv定义为网络套接字地址结构体(internet socket address struct),这时serv.sin_addr是一个in_addr结构体,serv.sin_addr.s_addr则是一个in_addr_t(32bit无符号整形数)地址。
struct sockaddr
{
uint8_t sa_len;
sa_family_t sa_family;//地址家族
char sa_data[14];//协议指定地质
};
所有的socket函数中,套接字结构体指针sockaddr_in总是需要强制转换为sockaddr类型,然后才作为作为参数传递进函数来,这时为什么呢,函数又是如何实现这种转换呢?因为函数套接字并不是只有sockaddr_in这一种套接字结构体,还有例如sockaddr_un的套接字结构体,需要让函数支持多种结构体,所以需要进行强制转换。
struct sockaddr 是一个通用地址结构,用于存储参与套接字通信的计算机上的一个internet协议(IP)地址。为了统一地址结构的表示方法 ,统一接口函数,使得不同的地址结构可以被bind()、connect()、recvfrom()、sendto()等函数调用。两者大小都是16字节(可以自己算一下),所以二者之间可以进行切换。
我们甚至可以用C++做个不太准确的假设,sockaddr是base class ,sockaddr_in等是derived class
如此一来,bind, connect,recvfrom等函数就可以使用base class 来处理多种不同的derived class了。
但是实际上,这是没有继承关系数据结构(不是C++),所以需要强制造型来转换数据类型。正因为如此,在sendto的时候需要给出len长度,因为不同的sockaddr_xx实现长度并不相同。
第二:(Value-result Argument)
可能很多人都已经注意到了,为什么在这两段程序中有一个socket函数和其他函数有一点不一样,accept函数,为什么它的第三个参数用的是地址结构体长度的指针,而如bind,connect函数的第三个参数则是地址结构地长度的值,这就是我们在这部分要说的值-结果参数,其实也就是结构提长度传递给寒暑的方式。
传递方式是由结构体传递的方向来决定的:从进程空间传到内核空间,或者从内核空间传导进程空间。
1.bind,connect,sendto函数的第三个参数都是都是结构体长度的值,因为这三个函数将结构体指针和结构体指针指向的结构体的大小都明确的传递给了内核空间,所以内核空间是非常清楚会从进程空间传递多少数据进来。
2.accept,recvfrom,getsockname和getpeername这四个函数从内核空间向进程空间传递值,它们的第三个参数是指向结构体长度的指针,
socklen_t clilen;
clilen=sizeof(cliaddr);
accept(listenfd,(struct sockaddr*)&cliaddr,&clilen);
之所以使用指针,很明显是因为clilen中存储的值应该是有变化的,调用函数的时候,是从进程空间向内核空间,这个参数告诉内核这个的结构体的大小,当函数返回的时候,从内核空间到进程空间,这个参数告诉进程空间内核空间实际上存储了多少结构体信息。
这个参数类型在复杂的网络编程,例如I/O端口复用中都会用到,用到的时候再进行深入的讨论,在这里我们只要指导使用结构体长度的指针的参数是因为进程空间和内核空间要交互一些信息。
第三:网络字节序和主机字节序(大端和小端)
大端和小端的概念我们就不说了,我会写一篇大端和小端的专门的文章,与union关键字结合一起讲解,我们知道,网络字节序一般大端的数据结构,而主机的字节序则根据操作系统和计算机架构的不通而不同,所以我们必须再网络传输过程中指定一种字节序,例如,再一个TCP报文中,有16bit的端口号和32bit的地址,发送协议栈必须和接收协议栈在字节序方面达成一致。
所以在tcpserv.cpp中我们使用htonl,htons函数,
servaddr.sin_port=htons(SERV_PORT);
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
而在tcpcli.cpp中我们使用了inet_pton(AF_INET,argv[1],&servaddr.sin_addr);
#include <arpa/inet.h>
int inet_pton(int family,const char *strptr,void *addrptr);
const char *inet_ntop(int family,const void *addrptr,char *strptr,size_t len);
inet_pton函数不仅能进行点分十进制和二进制地址之间的转换,还能进行网络字节序和主机字节序之间的转换
第四。基本的网络套接字函数及其特殊知识点
#include <sys/socket.h>
int socket(int family,int type,int protocol)
int connect(int sockfd,const struct sockaddr *servaddr,socklen_t addrlen);
connect函数在初始化三次握手协议的时候,如果在其发出SYN数据报后,如果在总共75秒的时间内没有接到回应,就会返回错误,这块我将单独写一篇关于connect阻塞的解决办法的文章。
int bind(int sockfd,const struct sockaddr *servaddr,socklen_t addrlen);
在这里我不再介绍OSI七层网络模型,或者TCP/IP四层协议栈,随着你不断的学习使用各种网络协议的通信程序的C语言编程,你会对他们非常熟悉的。
下图是基于TCP协议的客户端/服务器程序的一般流程
图解:
建立连接的过程:
服务器调用socket(),bind(),listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,而客户端调用socket()初始化以后,调用connect()发出SYN并等待服务器应答,服务器应答一个SYN-ACK,客户端收到后从connect返回,同时应答一个ACK,服务器收到后从accept返回。
数据传输的过程:
建立连接后,TCP协议提供全双工通信服务,一般情况下是由客户端主动发起请求,服务器被动处理请求,交互方式,因此,服务器从accept返回后立刻调用read()函数,读取socket,如果没有数据到达就阻塞等待,这时客户端调用write()函数发送请求给服务器,服务器收到后从read()函数返回,对客户端的请求进行处理,在此期间客户端调用read()函数阻塞等待服务器的应答,服务器调用write()函数将处理结果发回客户端,再次调用read()函数等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环。
关闭连接过程:
如果客户端没有更多请求了,就调用close()关闭连接,发送FIN信号到服务器端,服务器的read()函数返回0,这样服务器端就知道了客户端关闭了连接,也调用close()函数关闭连接,注意,这时服务器也发送一个FIN信号给客户端,很多著作和博客上说任何一方调用close()关闭连接后,连接的两个传输方向就都关闭了,不能再发送数据了,这时不准确的,关于这一点,在下一篇博客《linux简单的TCP C交互编程》一文中将介绍到。
我这里先写一个客户端和服务器端只进行一次性数据传输就各自关闭连接的例子,只是为了讲解上面的一般流程图,读者可以完全按照图中的步骤和程序一步一步对应起来,专注于连接过程。
源代码:
tcpcli.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define MAXLINE 80
#define SERV_PORT 9877
int main(int argc,char *argv[])
{
int num,sockfd;
struct sockaddr_in servaddr;
const char sendline[MAXLINE]="hello world";
char recvline[MAXLINE];
if(argc!=2)
{
printf("usage:tcpcli <IPaddress>");
}
sockfd=socket(AF_INET,SOCK_STREAM,0);
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(SERV_PORT);
bc39
inet_pton(AF_INET,argv[1],&servaddr.sin_addr);
connect(sockfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
write(sockfd,sendline,MAXLINE);
read(sockfd,recvline,MAXLINE);
printf("received from serve %s\n",recvline);
close(sockfd);
}
tcpserv.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAXLINE 80
#define SERV_PORT 9877
int main(int argc,char *argv[])
{
int listenfd,connfd;
struct sockaddr_in servaddr,cliaddr;
socklen_t clilen;
char recvline[MAXLINE];
listenfd=socket(AF_INET,SOCK_STREAM,0);
servaddr.sin_family=AF_INET;
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
servaddr.sin_port=htons(SERV_PORT);
bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
listen(listenfd,20);
connfd=accept(listenfd,(struct sockaddr *)&cliaddr,&clilen);
read(connfd,recvline,MAXLINE);
printf("received from client %s\n",recvline);
write(connfd,recvline,MAXLINE);
close(connfd);
}
我这里的所有源文件都使用g++编译器进行编译,因为g++编译器比gcc编译器在语法检查方面更加严格。
可以看到,服务器端再接收到客户端传来的数据后将其原封不动的传递回了客户端,然后客户端和服务器端分别退出。
[wind@windyang interview]$ ./tcpcli 127.0.0.1
received from serve hello world
[wind@windyang interview]$ ./tcpserv
received from client hello world
当然,再测试的过程中需要先启动服务器,再启动客户端,因为是客户端主动发起连接的。
下面我们从五个方面介绍一下在两段源代码中用到的技术和需要注意的地方:
第一:套接字地址结构体
我们要研究linux网络编程,首先要对其定义的socket进行,而对socket进行研究,就首先要研究套接字地址结构
struct in_addr
{
in_addr_t s_addr;//32位ipv4地址,网络字节序
};
struct sockaddr_in
{
uint8_t sin_len;//本结构体的长度
sa_family_t sin_family;//AF_INET
in_port_t sin_port;//16bit TCP或者UDP端口号
struct in_addr sin_addr;//32bit IPV4地址,网络字节序
char sin_zero[8];
};
可能大家看到这些例如sa_family_t等的数据类型,会感到非常陌生,我下面给出一张表格来解释一下这些类型的定义:
数据类型 | 描述 | 头文件 |
int8_t | 有符号8bit整形数 | <sys/types.h> |
uint8_t | 无符号8bit整形数 | <sys/types.h> |
int16_t | 有符号16bit整形数 | <sys/types.h> |
unit16_t | 无符号16bit整形数 | <sys/types.h> |
int32_t | 有符号32bit整形数 | <sys/types.h> |
uint32_t | 无符号32bit整形数 | <sys/types.h> |
sa_family_t | 套接字结构体地址家族,如果结构体中支持结构体长度数据段,是8bit无符号数,如果不支持的情况下是16bit无符号数。 | <sys/types.h> |
socklen_t | 套接字地址结构体的长度,通常是无符号32bit整形数 | <sys/types.h> |
in_addr_t | IPv4地址,通常是无符号32bit整形数 | <sys/types.h> |
in_port_t | TCP,UDP端口号,通常是无符号16bit整形数 | <sys/types.h> |
32bit的IPv4地址可以使用两种不同的方式进行操作:例如,如果serv定义为网络套接字地址结构体(internet socket address struct),这时serv.sin_addr是一个in_addr结构体,serv.sin_addr.s_addr则是一个in_addr_t(32bit无符号整形数)地址。
struct sockaddr
{
uint8_t sa_len;
sa_family_t sa_family;//地址家族
char sa_data[14];//协议指定地质
};
所有的socket函数中,套接字结构体指针sockaddr_in总是需要强制转换为sockaddr类型,然后才作为作为参数传递进函数来,这时为什么呢,函数又是如何实现这种转换呢?因为函数套接字并不是只有sockaddr_in这一种套接字结构体,还有例如sockaddr_un的套接字结构体,需要让函数支持多种结构体,所以需要进行强制转换。
struct sockaddr 是一个通用地址结构,用于存储参与套接字通信的计算机上的一个internet协议(IP)地址。为了统一地址结构的表示方法 ,统一接口函数,使得不同的地址结构可以被bind()、connect()、recvfrom()、sendto()等函数调用。两者大小都是16字节(可以自己算一下),所以二者之间可以进行切换。
我们甚至可以用C++做个不太准确的假设,sockaddr是base class ,sockaddr_in等是derived class
如此一来,bind, connect,recvfrom等函数就可以使用base class 来处理多种不同的derived class了。
但是实际上,这是没有继承关系数据结构(不是C++),所以需要强制造型来转换数据类型。正因为如此,在sendto的时候需要给出len长度,因为不同的sockaddr_xx实现长度并不相同。
第二:(Value-result Argument)
可能很多人都已经注意到了,为什么在这两段程序中有一个socket函数和其他函数有一点不一样,accept函数,为什么它的第三个参数用的是地址结构体长度的指针,而如bind,connect函数的第三个参数则是地址结构地长度的值,这就是我们在这部分要说的值-结果参数,其实也就是结构提长度传递给寒暑的方式。
传递方式是由结构体传递的方向来决定的:从进程空间传到内核空间,或者从内核空间传导进程空间。
1.bind,connect,sendto函数的第三个参数都是都是结构体长度的值,因为这三个函数将结构体指针和结构体指针指向的结构体的大小都明确的传递给了内核空间,所以内核空间是非常清楚会从进程空间传递多少数据进来。
2.accept,recvfrom,getsockname和getpeername这四个函数从内核空间向进程空间传递值,它们的第三个参数是指向结构体长度的指针,
socklen_t clilen;
clilen=sizeof(cliaddr);
accept(listenfd,(struct sockaddr*)&cliaddr,&clilen);
之所以使用指针,很明显是因为clilen中存储的值应该是有变化的,调用函数的时候,是从进程空间向内核空间,这个参数告诉内核这个的结构体的大小,当函数返回的时候,从内核空间到进程空间,这个参数告诉进程空间内核空间实际上存储了多少结构体信息。
这个参数类型在复杂的网络编程,例如I/O端口复用中都会用到,用到的时候再进行深入的讨论,在这里我们只要指导使用结构体长度的指针的参数是因为进程空间和内核空间要交互一些信息。
第三:网络字节序和主机字节序(大端和小端)
大端和小端的概念我们就不说了,我会写一篇大端和小端的专门的文章,与union关键字结合一起讲解,我们知道,网络字节序一般大端的数据结构,而主机的字节序则根据操作系统和计算机架构的不通而不同,所以我们必须再网络传输过程中指定一种字节序,例如,再一个TCP报文中,有16bit的端口号和32bit的地址,发送协议栈必须和接收协议栈在字节序方面达成一致。
所以在tcpserv.cpp中我们使用htonl,htons函数,
servaddr.sin_port=htons(SERV_PORT);
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
而在tcpcli.cpp中我们使用了inet_pton(AF_INET,argv[1],&servaddr.sin_addr);
#include <arpa/inet.h>
int inet_pton(int family,const char *strptr,void *addrptr);
const char *inet_ntop(int family,const void *addrptr,char *strptr,size_t len);
inet_pton函数不仅能进行点分十进制和二进制地址之间的转换,还能进行网络字节序和主机字节序之间的转换
第四。基本的网络套接字函数及其特殊知识点
#include <sys/socket.h>
int socket(int family,int type,int protocol)
int connect(int sockfd,const struct sockaddr *servaddr,socklen_t addrlen);
connect函数在初始化三次握手协议的时候,如果在其发出SYN数据报后,如果在总共75秒的时间内没有接到回应,就会返回错误,这块我将单独写一篇关于connect阻塞的解决办法的文章。
int bind(int sockfd,const struct sockaddr *servaddr,socklen_t addrlen);
相关文章推荐
- [代码复用]至少要考虑下简单的代码复用
- 超级简单好用的JS日期控件
- ASP.NET AJAX入门系列(6):UpdateProgress控件简单介绍
- 一个简单的使用SOAP协议调用Webservice实现
- 简单Js脚本用于Html中绝对位置的控件定位
- 如何简单更好的进行文章伪原创
- 学习python 第一季:编写简单乘法运算题
- 9、实现虚拟机与PC机共享最简单方法
- js验证只能为数字 和 简单的身份证号码验证。。
- win7简单命令让PC变身无线热点,共享上网
- C语言--简单小代码1
- Linux的简单使用
- Kinect实现简单的三维重建
- HTML 简单教程-实例004 HTML链接<a>标签
- Oracle多表的简单查询
- UVa 10220 I Love Big Numbers ! (简单高精度)
- 用c写的一个简单web服务器实现 转载来自sdlcwangsong
- PHP的MVC模式实现原理分析(一相简单的MVC框架范例)
- 简单Factory模式
- 1.最简单开始