天天看点

Unix网络编程【3】-基本TCP套接字1.基本TCP套接字函数2.并发服务器

本博客参考《Unix网络编程:卷1》

1.基本TCP套接字函数

1.1 socket函数

Ubuntu:/usr/include/x86_64-linux-gnu/sys/socket.h-函数原型
Ubuntu:/usr/include/x86_64-linux-gnu/bits/socket.h-可选参数
#include <sys/socket.h>
int socket(int family, int type,int protocol);
返回值:成功返回非负的描述符,出错返回-1
           

family-指明协议族

family 说 明
AF_INET IPv4协议
AF_INET6 IPv6协议
AF_LOCAL Unix域协议
AF_ROUTE 路由套接字
AF_KEY 密钥套接字

type-指明套接字类型

type 说 明
SOCK_STREAM 字节流套接字
SOCK_DGRAM 数据报套接字
SOCK_SEQPACKET 有序分组套接字
SOCK_RAW 原始套接字

protocol-指明协议的类型

protocol 说 明
IPPROTO_CP TCP传输协议
IPPROTO_UDP UDP传输协议
IPPROTO_SCTP SCTP传输协议

其中SCTP是一种全新的协议,它在客户和服务器之间提供关联,并向TCP那样给应用程序提供可靠、排序、流量控制以及全双工的数据传送。但是SCTP是面向消息的。相当与是对TCP和UDP的结合。

family和type参数的组合种类如下表所示

type\family AF_INET AF_INET6 AF_LOCAL AF_ROUTE AF_KEY
SOCK_STREAM TCP|SCTP TCP|SCTP
SOCK_DGRAM UDP UDP
SOCK_SEQPACKET SCTP SCTP
SOCK_RAW IPv4 IPv6

有些socket的调用family参数可能会使用以PF为前缀的参数,AF前缀和PF前缀的参数后面的字符是相同的。现在一般都是使用AF作为前缀的参数。PF-protocol family(协议族),AF-address family(地址族)。

1.2 connect函数

#include <sys.socket.h>
int connect(int sockfd,const struct sockaddr *servaddr, socklen_t addrlen);
返回:成功返回0,出错返回-1
           

在TCP通信中,客户通过该函数与TCP服务器建立连接。

sockfd-由socket函数返回的套接字描述符。

servaddr-指向套接字地址结构的指针。

addrlen-套接字地址结构的大小。

客户在调用此函数之前若没有调用bind,此函数会给客户自动分配IP和端口号。connect调用会激发TCP的三次握手过程。调用connect可能会出现错误的情况:

(1)若TCP客户没有收到SYN分节的响应,则返回ETIMEDOUT错误。TCP三次握手参考:(https://blog.csdn.net/qq_37981695/article/details/104706673)

(2)若对客户的SYN的响应是RST(表示复位),则表明该服务器主机在我们指定的端口上没有等待的连接。这是一种硬错误,客户一接收到RST就马上返回ECONNREFUSED错误。产生RST的三个条件①本条说明的情况。②TCP想取消一个已有连接。③TCP接收到一个根本不存在的连接上的分节。

(3)若客户发出的SYN在中间的某个路由器上引发了一个“destination unreachable”ICMP错误,则认为这是一中软错误。客户主机内核保存该信息,并按一定时间间隔继续发送SYN。若规定时间仍未收到响应,则把保存的消息(ICMP错误)作为EHOSTUNREACH或ENETUNREACH错误返回给进程。

1.3 bind函数

#include <sys/socket.h>
int bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrlen);
返回:若成功则为0,若错误则为-1
           

sockfd-由socket函数返回的套接字描述符。

servaddr-指向套接字地址结构的指针。

addrlen-套接字地址结构的大小。

bind函数可以进行的绑定操作有以下几种:

IP地址 端口 结果
通配地址 内核选择IP地址和端口
通配地址 非0 内核选择IP地址,进程指定端口
本地IP地址 进程指定IP地址,内核选择端口
本地IP地址 非0 进程指定IP地址和端口

如果指定端口号为0,内核就在bind被调用时选择一个临时端口。如果指定地址为通配地址,那么内核将等到套接字已连接(TCP)或已在套接字上发出数据报(UDP)时选择一个本地IP地址。

IPv4的通配地址设置如下:

struct sockaddr_in servaddr;
seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
           

htonl函数的作用可参考:(https://blog.csdn.net/qq_37981695/article/details/106071958)

//Ubuntu:/usr/include/netinet/in.h
typedef uint32_t in_addr_t;
#define	INADDR_ANY		((in_addr_t) 0x00000000)
           

IPv6的通配地址设置如下:

struct sockaddr_in6 serv;
serv.sin6_addr = in6addr_any;
           

由于IPv6的IP地址存放在一个结构中,所以我们无法之间赋值,需要使用结构为其赋值。struct sockaddr_in6结构可参考:(https://blog.csdn.net/qq_37981695/article/details/105967826)

//Ubuntu:/usr/include/netinet/in.h
//in6_addr与一般的书中介绍的有些不同,但是使用方法一样
struct in6_addr
  {
    union
      {
	uint8_t	__u6_addr8[16];
	uint16_t __u6_addr16[8];
	uint32_t __u6_addr32[4];
      } __in6_u;
#define s6_addr			__in6_u.__u6_addr8
#ifdef __USE_MISC
# define s6_addr16		__in6_u.__u6_addr16
# define s6_addr32		__in6_u.__u6_addr32
#endif
  };
extern const struct in6_addr in6addr_any;        /* :: */
           

1.4 listen函数

#include <sys/socket.h>
int listen(int sockfd,int backlog);
//返回:成功返回0,出错返回-1
           

listen函数主要做两件事。

(1)当socket创建一个套接字时,它是一个主动套接字。当使用listen之后,这个套接字被转换成了被动监听套接字。内核会指示套接字接收连接请求。

(2)第二个参数规定了内核应该为相应套接字排队的最大连接数目。

本函数的调用在socket和bind之后,在accept之前。监听套接字维护着两个分组:未完成连接队列(队列中的套接字未完成三次握手处于SYN_RCVD状态)、已完成连接队列(队列中的套接字已完成连接处于ESTABLISHED状态)。TCP的状态转移参考博客:(https://blog.csdn.net/qq_37981695/article/details/104706673)

Unix网络编程【3】-基本TCP套接字1.基本TCP套接字函数2.并发服务器

每当在未完成连接队列中创建一项时,来自监听套接字的参数就会赋值到即将建立的连接中。连接的创建机制是完全自动的,无需服务器进程插手。

注意事项:不要将backlog设置为0,因为不同的实现对0有不同的解释。

1.5 accept函数

#include <sys/socket.h>
int accept(int sockfd,struct sockaddr *cliaddr,socklen_t *addrlen);
//返回:成功返回非负描述符,出错返回-1
           

accept函数由TCP服务器调用,用于从已完成连接队列头返回下一个已完成连接。如果已完成队列为空,且套接字为阻塞模式,那么进程进行睡眠。

参数cliaddr和addrlen用来返回已连接的对端进程的协议地址。其中addrlen参数使用之前需要指定值。如果我们对客户端的地址不感兴趣可以将cliaddr和addrlen置为NULL。

accept返回的是已连接套接字而第一个参数是监听套接字。监听套接字在服务器运行的整个生命周期都存在,而已连接套接字在服务器完成对客户端的服务之后就会关闭。

1.6 close函数

//Ubuntu:/usr/include/unistd.h
#include <unistd.h>
int close(int sockfd);
//返回:成功返回0,出错返回-1
           

close一个TCP套接字会将套接字标记为关闭,然后立即返回到调用进程。该套接字描述符不能再由调用进程使用,也就是说不能再作为read和write的第一个参数。但是发送队列的数据还会发送,发送完毕之后就是正常的TCP的4次挥手。可以参考博客:(https://blog.csdn.net/qq_37981695/article/details/104706673)

close的关闭只是减少相应描述符的应用计数,在多进程或者多线程的程序中,引用计数较多的情况。即使调用close也不会对TCP连接关闭。这个时候可以使用shutdown代替close,shutdown的调用一定会引起4次挥手的过程。

1.7 getsockname和getpeername函数

#include <sys/socket.h>
int getsockname(int sockfd,struct sockaddr *localaddr,socklen_t *addrlen);
int getpeername(int sockfd,struct sockaddr *peeraddr,socklen_t *addr);
//返回:成功返回0,出错返回-1
           

getsockname-返回某个套接字的本地协议地址。

getpeername-返回某个套接字的对端协议地址。

这两个函数的应用场景

(1)当TCP客户没有调用bind时,connect成功返回,可以使用getsockname返回本地的IP地址和端口号。

(2)在以端口0调用bind后,getsockname用于返回由内核赋予的本地端口。

(3)getsockname可用于获取某个套接字的协议族。

(4)在使用通配地址调用bind的服务器上,accept返回后,可使用getsockname返回本地地址。这个套接字参数必须是accept返回的套接字。

(5)当一个服务器是由调用过accept的某个进程通过调用exec执行程序时,它能够获取客户身份的唯一途径便是调用getpeername。这个涉及到多进程。

1.8 基本TCP客户服务器程序创建流程

Unix网络编程【3】-基本TCP套接字1.基本TCP套接字函数2.并发服务器

​ 此流程是相对与阻塞的套接字而言的。

2.并发服务器

2.1 fork函数

#include <unistd.h>
pid_t fork(void);
//返回:在子进程中为0,在父进程中为子进程ID,若错误则返回-1
           

函数的说明:

(1)fork的调用会返回两个,在子进程中返回0,在父进程中返回子进程ID。这样设计的原因是子进程只有一个父进程且可以通过getppid获取父进程ID,而父进程有多个子进程且无法获取子进程ID。

(2)父进程中调用fork之前的所有描述符在fork返回之后由子进程共享。也就是说父子进程共享套接字描述符。

fork的典型用法:

(1)一个进程创建自身的副本,这样每个副本都可以在另一个副本执行其它任务的同时处理各自的操作。

(2)一个进程想要执行另一个程序。可以首先调用fork创建一个副本,然后其中的一个副本调用exec把自身替换成新的程序。exec把当前进程映像替换成新的程序文件,而且该新进程通常从main函数开始执行。进程的ID并不改变。我们称调用exec的进程为调用进程,称新执行的程序为新程序。

2.2 exec函数

#include <unistd.h>
int execl (const char *__path, const char *__arg, ...);
int execv (const char *__path, char *const __argv[]);
int execle (const char *__path, const char *__arg, ...);
int execve (const char *__path, char *const __argv[],char *const __envp[]) ;
int execlp (const char *__file, const char *__arg, ...);
int execvp (const char *__file, char *const __argv[]);
//返回:成功不返回,出错返回-1
           

这6个函数之间的区别:

(a)待执行的程序文件是由文件名还是路径名。

(b)新程序的参数是一一列出还是由一个指针数组来引用。

(c)把调用进程的环境传递给新程序还是给新程序指定新环境。

Unix网络编程【3】-基本TCP套接字1.基本TCP套接字函数2.并发服务器

一般来说,只有execve是内核中的系统调用,其它5个都是调用execve的库函数。

对这六个函数的说明:

(1)上面的3个函数把新程序的每个参数字符串指定为exec的一个独立参数,并以空指针结束这些可变参数。下面的3个函数将exec的参数放入独立变量argv,且这个指针数组的末尾是个空指针。

(2)左列2个函数指定一个filename参数。exec使用当前的PATH环境变量将该文件名参数转换为一个路径名。但是如果filename中含有"/"时,PATH中的内容就不再使用。右两列指定一个全限定的pathname参数。

(3)左两列4个函数不显示指定环境指针。相反,它们使用外部变量environ的当前值来构造一个传递给新程序的环境列表。右列2个函数显示指定一个环境列表,其envp指针数组必须以一个空指针结束。

默认情况下,进程在调用exec之前打开的描述符通过跨exec继续保持打开。除非使用fcntl设置FD_CLOEXEC描述符标志禁止掉。

2.3 并发服务器

标准的并发服务器程序

pid_t pid;
int listenfd,connfd;
listenfd = socket(...);
bind(listenfd,...);
listen(listenfd,LISTENQ);
for(;;){
	connfd = accept(listenfd,...);
	if((pid=fork())==0){
		close(listenfd);
		doit(connfd);
		close(connfd);
		exit(0);
	}
	close(connfd);
}
           

在子进程或者父进程close不需要的描述符的原因是套接字描述符是跨进程打开的,直接导致描述符的引用计数+1,如果在不使用该描述符的进程不调用close,那么该描述符的引用计数就无法置为0,该套接字就无法关闭。

并发服务器的建立流程

Unix网络编程【3】-基本TCP套接字1.基本TCP套接字函数2.并发服务器

继续阅读