Linux -TCP网络编程基础
一.套接字编程基础知识
1.套接字地址结构
套接字编程需要指定套接字的地址作为形参,不同的协议族有不同的地址结构定义方式,这些地址结构通常以sockaddr_
开头,每一个协议族有一个唯一的后缀,例如以太网,其结构名称为sockaddr_in
。
1.套接字数据结构
通用套接字地址类型如下,可以在不同协议族之间进行强制转换:
struct sockaddr{
//套接字地址结构
sa_family_t sa_family; //协议族
char sa_data[14]; //协议族数据
}
注:sa_family_t 类型为unsigned short
类型,长度为16字节。
typedef unsigned short sa_faily_t;
2.实际使用的套接字数据结构
网络程序设计中所使用的函数中几乎所有的套接字函数都用这个结构作为参数,例如bind()函数:
int bind(int sockfd,//套接字文件描述符
const struct sockaddr *my_addr,//套接字地址结构
socklen_t addrlen);//套接字地址结构的长度
使用struct sockaddr不方便进行设置,以太网中使用struct sockaddr_in进行设置,如下:
struct sockaddr_in{
//以太网套接字地址结构
u8 sin_len;//结构struct sockaddr_in长度,16
u8 sin_family;//通常为AF_INET
u16 sin_port;//16位的端口号,网络字节序
struct in_addr sin_addr;//IP地址为32位
char sin_zero[8];//未用
};
结构struct sockaddr_in
的成员变量in_addr
用于表示IP
地址,这个结构定义如下:
struct in_addr{
//IP地址结构
u32 s_addr;//32位IP地址,网络字节序
};
3.结构sockaddr和结构sockaddr_in关系
结构struct sockaddr和结构struct sockaddr_in是一个同样大小的结构,对应关系如下:
struct sockaddr_in的成员含义如下:
-
sin_len: 无符号字符类型,表示结构struct sockaddr _in的长度,为16。
-
sin_family: 无符号字符类型,通常设置为与socket()函数的domain一致,例如 AF_INET。
-
sin_port: 无符号short类型,表示端口号,网络字节序。
-
sin_addr: struct in_addr类型,其成员s_addr为无符号32位数,每8位表示IP地址的一 个段,网络字节序。
-
sin_zero[8]: char类型,保留。
进行地址结构设置时,通常的方法是利用结构struct sockadd _in
进行设置,然后强制转换为结构struct sockaddr
类型。因为这两个结构大小是完全一 致的,所以进行这样的转换不会有副作用。
2.用户层和内核层交互过程
-
套接字参数中有部分参数是需要用户传入的,这些参数用来与Linux内核进行通信,例如指向地址结构的指针。通常是采用内存复制的方法进行。
- 例如bind()函数需要传入地址结构
struct sockaddr *my_ addr
和my_addr
指向参数的长度。
- 例如bind()函数需要传入地址结构
1.向内核传入数据的交换过程
-
向内核传入数据的函数有send()、bind()等,从内核得到数据的函数有accept()、recv()等。
-
传入的过程如下图所示,bind()函数向内核中传入的参数有套接字地址结构和结构的长度两个与地址结构有关的参数。
-
参数addlen表示地址结构的长度,参数my_addr是指向地址结构的指针。
-
调用函数bind()的时候,地址结构通过内存复制的方式将其中的内容复制到内核,地址结构的长度通过传值的方式传入内核,内核按照用户传入的地址结构长度来复制套接字地址结构的内容。
2.内核传出数据的交换过程
-
从内核向用户空间传递参数的过程则相反,传出的过程如下图所示。
-
通过地址结构的长度和套接字地址结构指针来进行地址结构参数的传出操作。
-
通常是两个参数完成传出作的功能,一个是表示地址结构长度的参数,另一个是表示套接字地址结构地址的指针。
-
传出与传入中的参数有所不同,表示地址结构长度的参数在传入过程中是传值,而在传出过程中是通过传址完成的。
-
内核按照用户传入的地址结构长度进行套接字地址结构数据的复制,将内核中的地址结构数据复制到用户传入的地址结构指针中。
二.TCP网络编程流程
1.TCP网络编程架构
TCP网络编程有两种模式:
-
服务器模式:服务器模式创建一个服务程序,等待客户端用户的连接,接收到用户的连接请求后,根据用户的请求进行处理;
-
客户端模式:客户端模式则根据目的服务器的地址和端口进行连接,向服务器发送请求并对服务器的响应进行数据处理。
1.服务器端的程序设计模式
流程主要分为:
- 套接字初始化(socket()函数)
- 套接字与端口的绑定(bind()函数)
- 设置服务器的侦听连接(listen()函数)
- 接受客户端连接(accept()函数)
- 接收和发送数据(read()函数、write()函数)
- 数据处理及处理完毕的套接字关闭(close()函数)
下图为TCP连接的服务器模式的程序设计:
-
套接字初始化根据用户对套接字的需求来确定套接字的选项。这个过程中的函数为socket(),它按照用户定义的网络类型、协议类型和具体的协议标号等参数来定义。系统根据用户的需求生成一个套接字文件描述符供用户使用。
-
套接字与端口的绑定将套接字与一个地址结构进行绑定。绑定之后,在进行网络程序设计的时候,套接字所代表的IP地址和端口地址,以及协议类型等参数按照绑定值进行操作。
-
服务器需要满足多个客户端的连接请求,而服务器在某个时间仅能处理有限个数的客户端连接请求,所以服务器需要设置服务端排队队列的长度。服务器侦听连接会设置这个参数,限制客户端中等待服务器处理连接请求的队列长度。
-
在客户端发送连接请求之后,服务器需要接收客户端的连接,然后才能进行其他的处理。
-
在服务器接收客户端请求之后,可以从套接字文件描述符中读取数据或者向文件描述符发送数据。接收数据后服务器按照定义的规则对数据进行处理,并将结果发送给客户端。
-
当服务器处理完数据,要结束与客户端的通信过程的时候,需要关闭套接字连接。
2.客户端的程序设计模式
主要分为:
- 套接字初始化(socket()函数)
- 套接字初始化(socket()函数)
- 连接服务器(connect()函数)
- 读写网络数据(read()函数、write()函数)
- 数据处理和最后的套接字关闭(close()函数)
如下图所示为TCP客户端模式:
- 客户端程序设计模式流程与服务器端的处理模式流程类似
-
不同之处是客户端在套接字初始化之后可以不进行地址绑定,而是直接连接服务器端。
- 客户端连接服务器的处理过程中,客户端根据用户设置的服务器地址、端口等参数与特定的服务器程序进行通信。
3.客户端与服务器的交互过程
客户端与服务器在连接、读写数据、关闭过程中有交互过程:
-
客户端的连接过程,对服务器端是接收过程,在这个过程中客户端与服务器进行三次握手,建立TCP连接。建立TCP连接之后,客户端与服务器之间可以进行数据的交互。
-
客户端与服务器之间的数据交互是相对的过程,客户端的读数据过程对应了服务器端的写数据过程,客户端的写数据过程对应服务器的读数据过程。
-
在服务器和客户端之间的数据交互完毕之后,关闭套接字连接。
2.创建网络插口函数socket()
socket()函数原型如下:调用成功返回表示这个套接字的文件描述符,失败返回-1。
#incluede<sys/types.h>
#include<sys/socket.h>
int socket(int domain,int type,int protocol);//协议族domain;协议类型type;协议编号protocol
- 参数
domain
用于设置网络通信的域,函数根据此参数选择通信协议的族。通信协议族在文件sys/socket.h
中定义,包含下表所有值,以太网中应该使用PF_INET
这个域,现有代码使用AF_INET
这个值,在头文件中两个值是相同的。
- 参数
type
参数type
用于设置套接字通信的类型,如下表所示type格式定义及含义。主要有SOCK_STREAM(流式套接字)
、SOCK_DGRAM(数据包套接字)
等。
不是所有协议族都实现了这些协议,例:AF_INET
协议族就没有实现SOCK_SEQPACKET
协议类型。
- 参数
protocol
用于指定某个协议类型,即type类型中某个类型。通常某个协议中只有一种特定类型,这样protocol参数仅能设置为0。但有些协议有很多种特定的类型,就需要设置这个参数来选择特定的类型。
-
SOCK_STREAM
的套接字表示一个双向的字节流,与管道类似。流式的套接字在进行数据收发之前必须已经连接,连接使用connect()函数进行,连接成功使用read()或者write()函数进行数据的传输。流式通信方式保证数据不会丢失或者重复接收,当数据在一 段时间内仍然没有接收完毕,可以认为这个连接已经死掉。 -
SOCK_DGRAM
和SOCK_RAW
这两种套接字可以使用函数sendto()来发送数据,使用recvfrom()函数接收数据,recvfrom()接收来自指定IP地址的发送方的数据。 -
SOCK _PACKET
是一种专用的数据包,它直接从设备驱动接收数据。 -
函数socket()执行过程可能会出现错误,可以通过
ermo
获得,具体值和含义在如下表。 -
通常情况下造成函数socket()失败的原因是输入的参数错误造成的,例如某个协议不存在等,这时需要详细检查函数的输入参数。
-
由于函数的调用不一 定成功,在进行程序设计的时候,一定要检查返回值。
使用sockt()函数需要设置上述3个参数,如将socket()函数的第一个参数domain设置为AF_INET,第二个参数设置为SOCK_STREAM,第三个参数设置为0,建立一个流式套接字。
int sock = socket(AF_INET,SOCK_STREAM,0);
2.应用层函数socket()和内核函数之间关系
用户设置套接字参数后,函数能够起作用,需要与内核空间的相系统交互,应用层的socket()函数是和内核层的系统调用相对应的,如下图所示:
函数sock=socket(AF_INET,SOCK_STREAM,0)
,此函数调用系统函数sys_socket(AF_INET,SOCK_STREAM,0),(在文件net/socket.c中)
。系统调用函数分为两部分,一部分生成内核socket结构(z注意于应用层的socket()函数是不同的),另一部分与文件描述符绑定,将绑定的文件描述符值传递给应用层。内核sock结构如下(在文件linux/net.h
):
struct socket{
socket state;//socket状态(例如SS_CONNECTED等)
unsigned long flags;//socket标志(SOCK_ASYNC_NOSPACE等)
const struct proto_ops *ops;//协议特定的socket操作
struct fasync_struct *fasync_list;//异步唤醒列表
struct file *file;//文件指针
struct sock *sk;//内部网络协议结构
wait_queue_head_t wait;//多用户时的等待队列
short type;//socket类型(SOCK_STREAM等);
};
-
内核函数
sock_create()
根据用户的domain
指定协议族,创建一个内核socket结构绑定到当前的进程上,其中type
与用户空间用户的设置值是相同的。 -
sock_map_fd()
函数将socket结构与文件描述符列表中的某个文件描述符绑定,之后的操作可以查找文件描述符列表来对内核socket结构。
3.绑定一个地址端口对bind()
建立套接字文件描述符成功后,需对套接字进行地址和端口的绑定,才能进行数据的接收和发送操作。
1.bind()函数介绍
bind()函数将长度为addlen
的struct sockadd
类型的参数my_addr
与sockfd
绑定在一起,将socked绑定到某个端口上,如果使用connect()函数则没有绑定的必要。绑定的函数原型如下:
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd,const struct socket *my_addr,socklen_t addrlen);
-
sockfd:函数创建的文件描述符。
-
my_addr:指向一个结构为sockaddr参数的指针,sockaddr包含地址、端口、IP地址信息。绑定时需将地址结构中的IP地址、端口、类型等结构struct sockaddr中的域进行设置后才能进行绑定,绑定后才能将套接字文件描述符与地址等结合在一起。
-
addrlen:表示my_addr结构的长度,可以设置成sizeof(struct sockaddr)。一般使用AF_INET设置套接字的类型和其他对应的结构,但不同类型的套接字有不同的地址描述符,强制指定地址长度,可能造成不可预料的后果。
bind()函数的返回值为0时表示绑定成功,-1表示绑定失败,erron的错误值如下:
下面代码初始化一个AF_UNIX族中的SOCK_STREAM类型的套接字。先使用结构struct sockaddr_un初始化my_addr,然后绑定,结构stuct sockaddr_un定义如下:
struct sockaddr_un{
sa_family_t sun_family;//协议族,应该设置为AF_UNIX
char sun_path[UNIX_PATH_MAX];//路径名。UNIX_PATH_MAX的值为108
};
2.bind()函数的例子
使用bind()函数进行程序设计的一个实例,先建立一个UNIX族的流类型套接字,然后将套接字地址和套接字文件描述符进行绑定:
#define MY_SOCK_PATH "/somepath"
int main(int argc,char *argv[])
{
int sfd;
struct sockaddr_un addr;//AF_UNIX对应的结构
sfd = socket(AF_UNIX,SOCK_STREAM,0);//初始化一个AF_UNIX族的流类型socket;将协议族参数设置为AF_UNIX建立为UNIX族套接字,使用函数socket()进行建立。
if(sfd == -1)//检查是否正常初始化socket
{
perror("socket");
exit(EXIT_FAILURE);
}
memset(&addr,0,sizeof(struct sockaddr_un));//将变量addr置0;初始化地址结构,将UNIX地址结构设置为0,这是进行程序设计时常用的初始化方式
addr.sun_family = AF_UBIX;//协议族为AF_UNIX
strncpy(addr.sun_path,MY_SOCK_PATH,//复制路径到地址结构
sizeof(addr.sun_path - 1);
if(bind(sfd, (struct sockaddr *) &addr,//绑定
sizeof(struct sockaddr_un))==-1)//绑定并判断是否成功
{
perror("bind");
exit(EXIT_FAILURE);
}
...//数据接收发送及处理过程
close(sfd);//关闭套接字文件描述符
}
注:Linux 的GCC编译器有一个特点,一个结构的最后一个成员为数组时,这个结构可以通过最后一个成员进行扩展,可以在程序运行时笫一次调用此变量的时候动态生成结构的大小。例如上面的代码,并不会因为 struct sockaddr_un
比struct sockaddr
大而溢出。
另一个使用结构struct sockaddr_in绑定一个AF_INET族到流协议,先将结构struct sockaddr_in的sin_family设置为AF_INET,然后设置端口,接着设置一个IP地址,最后进行绑定:
#define MYPORT 3490 //端口地址
int main(int arg,char *argv[])
{
int sockfd;//套接字文件描述符变量
struct sockaddr_in my_addr;//以太网套接字地址结构
sockfd = socket(AF_INET,SOCK_STREAM,0);//初始化socket
if(sockfd == -1){
//检查是否正常初始化socket
perror("socket");
exit(EXIT_FAILURE);
}
my_addr.sin_family = AF_INET;//地址结构的协议族
my_addr.sin_port = htons(MYPORT);//地址结构的端口地址,网络字节序,使用htins()进行字节序转换。
my_addr.sin_addr.s_addr = inet_addr("192.168.1.150");//IP,将字符串的IP地址转化为网络字节序
bzero(&(my_addr.sin_zero),8);//将my_addr.sin_zero置为0
if(bind(sockfd,(struct sockaddr *)&myadd,
sizeof(struct sockaddr)) == -1){
//判断是否绑定成功
perror("bind");
exit(EXIT FAILURE);
}
...//接收和发送数据,进行数据处理
close(sockfd);//关闭套接字文件描述符
}
3.应用层bind()函数和内核函数之间关系
bind()是应用层函数,要使用函数生效,就要将相关的参数传递给内核并进行处理,应用层的bind()函数与内核之间的函数过程如下所示,图中是一个AF_INET族函数进行绑定的调用过程:
-
应用层的函数
bind(sockfd,(struct sockaddr*)&my_ addr, sizeof(struct sockaddr))
调用系统函数过程sys_bind(sockf d,(struct sockaddr*)&my_ addr, sizeof(struct sockaddr))
。 -
sys_bind()
函数首先调用函数sockfd_lookup _light()
来获得文件描述符sockfd对应的内核struct sock结构变量,然后调用函数move_addr_to_kemel()
将应用层的参数my_addr复制进内核,放到address变量中。 -
内核的sock结构是在调用socket()函数时根据协议生成的,它绑定了不同协议族的bind()函数的实现方法,在AF_INET族中的实现函数为
inet_bind()
, 即会调用AF_INET族的bind()函数进行绑定处理。
4.监听本地端口listen
函数listen()用来初始化服务器可连接队列,多个客户端连接请求同时到来时,服务器不会同时处理,而是将不能处理的客户端连接请求放到等待队列中,队列长度由listen()函数定义。
1.listen()函数介绍
listen()函数原型如下,其中的backlong表示等待队列的长度:
#include<sys/socket.h>
int listen(int sockfd,int backlog);
运行成功时,返回0,失败返回-1,并且设置erron值,错误代码含义如下:
- 接受连接之前,需要用 listen()函数来侦听端口,listen()函数中参数 backlog 的参数表示在 accept()函数处理之前在等待队列中的客户端的长度,如果超过这个长度,客户端会返回一 个
ECONNREFUSED
错误。 - listen() 函数仅对类型为
SOCK_STR EAM
或者SOCK _SEQPACKET
的协议有效,例如,如果对一 个SOCK_DGRAM
的协议使用函数 listen(), 将会出现错误 errno应该为值EOPNOTSUPP
, 表示此 socket不支持函数 listen()操作。大多数系统的设置为 20, 可以将其设置修改为 5或者10, 根据系统可承受负载或者应用程序的需求来确定。
2.listen()函数的例子
在成功进行socket()函数初始化和bind()函数1端口之后,设置listen()函数队列的长度为5。
#define MYPORT 3490 // 端口地址
int main(int argc,char *argv[])
{
int sockfd;//套接字文件描述符变量
struct sockaddr_in my_addr;
sockfd = socket(AF_INET