【Linux】Linux网络编程(含常见服务器模型,上篇)

版权声明:本文为博主原创文章,允许转载,但希望标注转载来源。 https://blog.csdn.net/qq_38410730/article/details/82084676

基本数据结构介绍

Linux系统是通过提供嵌套字(socket)来进行网络编程的。网络程序通过socket和其他几个函数的调用,会返回一个通用的文件描述符,用户可以将这个描述符看成普通的文件的描述符来操作,这就是Linux的设备无关性的好处。用户可以通过向描述符的读写操作实现网络之间的数据交流。

表示套接口的socket结构体

struct socket {
	socket_state		state;            //指明套接口的连接状态
	short			type;
	unsigned long		flags;

	struct fasync_struct	*fasync_list;
	wait_queue_head_t	wait;

	struct file		*file;            //指向sockfs文件系统中的相应文件
	struct sock		*sk;            //任何协议族都有其特定的套接口特性,该域就指向特定协议族的套接口对象
	const struct proto_ops	*ops;            //指明可对套接口进行的各种操作
};

描述套接口通用地址的数据结构sockaddr结构体

由于历史的缘故,在bind、connect等系统调用中,特定于协议的套接口地址结构指针都要强制转换成该通用的套接口地址结构指针。结构形式如下:

struct sockaddr {
	sa_family_t	sa_family;	/* address family, AF_xxx	*/
	char		sa_data[14];	/* 14 bytes of protocol address	*/
};

描述因特网地址结构的数据结构sockaddr_in(这里局限于IP4)

struct sockaddr_in {
        sa_family_t		sin_family;	    /* 描述协议族		*/
        __be16		sin_port;	        /* 端口号			*/
        struct in_addr	sin_addr;	    /* 因特网地址		*/

        unsigned char		__pad[__SOCK_SIZE__ - sizeof(short int) -
                sizeof(unsigned short int) - sizeof(struct in_addr)];
};

基本网络函数介绍

表头文件

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

socket()函数

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

函数说明:socket()用来建立一个新的socket,也就是向系统注册,通知系统建立一通信端口。

  • 参数domain指定协议域,又称为协议族。常用的协议族有:AF_INET、AF_INET6、AF_LOCAL、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址,完整的定义在usr/include/bits/socket.h内;
  • 参数type指定socket类型。常用的socket类型有:SOCK_STREAM(双向连续且可信的数据流,即TCP)、SOCK_DGRAM(不连续不可信赖的数据包连接)、SOCK_PACKET(和网络驱动程序直接通信)、SOCK_SEQPACKET(连续可信赖的数据包连接)等等;
  • 参数protocol指定协议。常用的协议有:IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

返回值:成功则返回socket描述符,失败则返回-1。

bind()函数

int bind(int sockfd, struct sockaddr * my_addr, int addrlen);

函数说明:bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。

  • 参数sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket;
  • 参数addr:一个指向sockaddr类型对象的指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同;
  • 参数addrlen:对应的是地址的长度。

返回值:成功则返回0,失败则返回-1。

对于不同socket的sockaddr,定义了一个通用的数据结构:

struct sockaddr {
	sa_family_t	sa_family;	/* address family, AF_xxx	*/
	char		sa_data[14];	/* 14 bytes of protocol address	*/
};

其中,sa_family为调用socket()时的domain参数,即AF_xxxx值;sa_data最多使用14个字符长度。

sockaddr结构会因使用不同的socket domain而有不同的结构定义,例如AF_INET:

struct sockaddr_in {
        sa_family_t    sin_family; /* address family: AF_INET */
        in_port_t      sin_port;   /* port in network byte order */
        struct in_addr sin_addr;   /* internet address */
};

struct in_addr {
        uint32_t       s_addr;     /* address in network byte order */
};

例如AF_INET6:

struct sockaddr_in6 { 
        sa_family_t     sin6_family;   /* AF_INET6 */ 
        in_port_t       sin6_port;     /* port number */ 
        uint32_t        sin6_flowinfo; /* IPv6 flow information */ 
        struct in6_addr sin6_addr;     /* IPv6 address */ 
        uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */ 
};

struct in6_addr { 
        unsigned char   s6_addr[16];   /* IPv6 address */ 
};

listen()函数

int listen(int sockfd, int backlog)

函数说明:sockfd参数为要监听的socket描述字,backlog参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen()函数将socket变为被动类型的,等待客户的连接请求。

返回值:成功则返回0,失败则返回-1。

注意:listen()只适合SOCK_STREAM或SOCK_SEQPACKET的socket类型。如果socket为AF_INET,则参数backlog的最大值为128。backlog不能限制连接的个数,只能限制后备连接(连接请求队列)的大小;一旦调用accept()函数处于请求队列里面的后备连接的数量就减一。

connect()函数

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

函数说明:sockfd参数为客户端的socket描述字,addr参数为服务器的socket地址,addrlen参数为socket地址的长度。客户端通过调用connect()函数来建立与TCP服务器的连接。

返回值:成功则返回0,失败返回-1,错误原因存于errno中。

accept()函数

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

函数说明:sockfd参数为服务器的socket描述字,addr参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。

返回值:如果accpet()成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。

注意:accept()的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept()函数返回的是已连接的socket描述字。一个服务器通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

服务器与客户端的信息函数

字节转换函数

区分一下网络字节序与主机字节序:

主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:

  • Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端;
  • Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

网络字节序:4个字节的32bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。

所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。

为了统一,在Linux下有专门的字节转换函数:

#include <netinet/in.h>

unsigned long int htonl(unsigned long int hostlong);        //将32位主机字节序转换成网络字节序
unsigned short int htons(unsigned short int hostshort);        //将16位主机字节序转换成网络字节序
unsigned long int ntohl(unsigned long int netlong);        //将32位网络字节序转换成主机字节序
unsigned short int ntohs(unsigned short int netshort);        //将16位网络字节序转换成主机字节序

在这四个转换函数中,h代表host,n代表network,l代表long,s代表short。

IP和域名的转换

在网络上,标志一台计算机可以用名字形式的网址,例如blog.csdn.net/qq_38410730,也可以使用地址的IP形式47.95.164.112,它是一个32位的整数,每个网络节点有一个IP地址,它唯一地确定一台主机,但一台主机可以有多个IP地址。

在网络中,通常组织运行多个名字服务器来提供名字与IP地址之间的转换,各种应用程序通过调用解析器库中的函数来与域名服务系统通信。常用的解析函数有:

struct hostent * gethostbyname(const char * hostname);            //名字地址转换为数字地址
struct hostent * gethostbyaddr(const char * addr, int len, int type);            //数字地址转换为名字地址

函数说明:第二个函数sddr参数为含有IP地址信息的in_addr结构的指针(为了同时传递IPv4之外的其他信息,设置为char*类型的);len参数为地址信息的字节数,IPv4为4,IPv6为16;type参数为地址族信息,IPv4为AF_INET,IPv6为AF_INET6。

返回值:两个函数失败时返回NULL且设置h_errno错误变量,调用h_strerrno()可以得到详细的错误信息。

其中,struct hostent的定义为:

struct hostent
{
        char *h_name;         //正式主机名
        char **h_aliases;     //主机别名
        int h_addrtype;       //主机IP地址类型:IPV4-AF_INET
        int h_length;		  //主机IP地址字节长度,对于IPv4是四字节,即32位
        char **h_addr_list;	  //主机的IP地址列表
};
	
#define h_addr h_addr_list[0]   //保存的是IP地址

例如:

#include <stdio.h>
#include <netdb.h>
#include <sys/socket.h>
#include <arpa/inet.h>
 
int main(int argc, char *argv[])
{
	char *ptr, **pptr;
	struct hostent *hptr;
	char str[32] = {0};
	ptr = argv[1];
	
	if((hptr = gethostbyname(ptr)) == NULL){
		printf("gethostbyname error: %s\n", ptr);
		return 0;
	}
	
	printf("official hostname:%s\n", hptr->h_name);   //主机规范名
	for(pptr = hptr->h_aliases; *pptr != NULL; pptr++)   //将主机别名打印出来
		printf("alias: %s\n", *pptr);
	
	switch(hptr->h_addrtype)  //根据地址类型,将地址打印出来
	{
		case AF_INET:
		case AF_INET6:
			pptr = hptr->h_addr_list;
		
			for(; *pptr != NULL; pptr++)   //将得到的所有地址打印出来
			{
				printf("address: %s\n", inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));   //inet_ntop: 将网络字节序的二进制转换为文本字符串的格式
				printf("first address: %s\n", inet_ntop(hptr->h_addrtype, hptr->h_addr, str, sizeof(str)));
			}
			break;
		default:
			printf("unkown address type\n");
			break;
	}
	
	return 0;
}

字符串的IP和32位的IP转换

网络上用的IP地址都是数字加点构成的,而在struct in_addr结构中用的是32位的IP,把数字加点类型转换成32位IP,可以使用下面两个函数:

int inet_aton(const char * cp, struct in_addr * inp);        //将数字加点类型转化成32位的IP,存储在inp指针里
char * inet_ntoa(struct in_addr in);        //将32位的IP转换成数字加点类型

函数中的a表示ASCII,n代表network。

注意与下面两个函数的区别:

in_addr_t inet_addr(const char *cp);        //将数字加点类型转化成32位的IP
in_addr_t inet_network(const char *cp);        //将数字加点类型转化成32位的IP

这两个函数与inet_aton()的区别在于:

这两个函数,当IP是255.255.255.255时,会认为这是个无效的IP地址,这是历史遗留问题,其实在目前大部分的路由器上,这个255.255.255.255的IP都是有效的。而inet_aton()函数认为255.255.255.255是有效的,它不会冤枉这个看似特殊的IP地址。

服务信息函数

在网络程序中,用户有时需要知道端口IP和服务信息,这个时候可以使用以下几个函数:

int getsockname(int sockfd, struct sockaddr *localaddr, int *addrlen);
int getpeername(int sockfd, struct sockaddr *peeraddr, int *addrlen);

struct servent *getservbyname(const char *servname, const char *protoname);            //某一个协议下的某个服务
struct servent *getservbyport(int port, const char *protoname);

struct servent
{
        char *s_name;            //正式服务名
        char **s_aliases;            //别名列表
        int s_port;            //端口号
        char *s_proto;            //使用的协议
}

例子:

struct servent *sptr;

sptr = getservbyname("domain", "udp");        // DNS using UDP
sptr = getservbyname("ftp", "tcp");        //FTP using TCP

sptr = getservbyport(htons(53), "udp");        // DNS using UDP
sptr = getservbyport(htons(21), "tcp");        //FTP using TCP

参考文章:网络编程学习笔记(getservbyname和getservbyport函数)

完整的读写函数

一旦用户建立了连接,下一步就是进行通信了。在Linux下,把用户前面建立的通道看作文件描述符,这样服务器端和客户端进行通信时,只要往文件描述符里面读写东西就可以了,就像用户往文件读写一样。

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);
ssize_t read(int fd, void *buf, size_t count);

函数说明:将数据写入已打开的文件内,write()会把参数buf所指的内存写入count个字节到参数fd所指向的文件内。如果顺利,write()会返回实际写入的字节数,当有错误发生时,则返回-1;

从已打开的文件读取数据,read()会把参数fd所指向的文件传送count个字节到buf指针所指的内存中。返回值为实际读取到的字节数。

例子:

//客户端向服务器端写
struct my_struct my_struct_client;
write(fd, (void *)&my_struct_client, sizeof(struct my_struct));

//服务器端的读
char buffer[sizeof(struct my_struct)];
struct *my_struct_server;
read(fd, (void *)buffer, sizeof(struct my_struct));
my_struct_server=(struct my_struct *)buffer;

需要注意的是:在网络上传递数据时,用户一般都是把数据转化为char类型的数据来传递。接收时也是一样。同时,用户没有必要在网络上传递指针(因为传递指针时没有任何意义的,用户必须传递指针所指向的内容)。

这里还要注意一下堵塞的问题:

在阻塞模式下, 对于TCP套接字(默认情况下),当使用 write() 发送数据时:

  • 首先会检查缓冲区,如果缓冲区的可用空间长度小于要发送的数据,那么 write() 会被阻塞(暂停执行),直到缓冲区中的数据被发送到目标机器,腾出足够的空间,才唤醒 write() 函数继续写入数据;
  • 如果TCP协议正在向网络发送数据,那么输出缓冲区会被锁定,不允许写入,write() 也会被阻塞,直到数据发送完毕缓冲区解锁,write() 才会被唤醒;
  • 如果要写入的数据大于缓冲区的最大长度,那么将分批写入;
  • 直到所有数据被写入缓冲区 write() 才能返回。

当使用 read() 读取数据时:

  • 首先会检查缓冲区,如果缓冲区中有数据,那么就读取,否则函数会被阻塞,直到网络上有数据到来;
  • 如果要读取的数据长度小于缓冲区中的数据长度,那么就不能一次性将缓冲区中的所有数据读出,剩余数据将不断积压,直到有 read() 函数再次读取;
  • 直到读取到数据后 read() 函数才会返回,否则就一直被阻塞。

这就是TCP套接字的阻塞模式。所谓阻塞,就是上一步动作没有完成,下一步动作将暂停,直到上一步动作完成后才能继续,以保持同步性。

用户数据报发送

之前的主要是基于TCP协议的网络程序,下面就主要介绍一下基于UDP协议的网络程序。

表头文件

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

recvfrom()函数

int recvfrom(int s, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen);

函数说明:经socket接收数据,recvfrom()用来接收远程主机指定的socket传来的数据,并把数据存到参数buf指向的内存空间,参数len为可接收数据的最大长度。参数flags一般设为0,参数from用来指定欲传送的网络地址,参数fromlen为sockaddr的结构长度。

返回值:成功则返回接收到的字符数,失败则返回-1。

sendto()函数

int sendto(int s, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen);

函数说明:经socket发送数据,sendto()用来将数据由指定的socket传给对方主机。参数s为已建好连线的socket,如果利用UDP协议则不需经过连线操作。参数msg指向欲连接线的数据内容,参数flags一般设为0,参数to用来指定欲传送的网络地址,参数tolen为sockaddr的结构长度。

返回值:成功则返回接收到的字符数,失败则返回-1。

下一篇文章:【Linux】Linux网络编程(含常见服务器模型,下篇)

猜你喜欢

转载自blog.csdn.net/qq_38410730/article/details/82084676