zl程序教程

您现在的位置是:首页 >  系统

当前栏目

Linux网络编程(五)-Socket编程04:TCP服务端流程【创建套接字/socket()、绑固定IP和端口/bind()、监听/listen()、创建通信套接字/accept()、读写、关闭】

2023-09-27 14:20:40 时间

TCP服务端通信步骤:

  • 创建socket套接字;
  • 绑定 bind
  • 监听 listen
  • 提取 accept
  • 读写
  • 关闭

socket模型创建流程图

一、TCP服务端函数

 1、socket()【创建socket的函数】

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

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

功能:

socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调用出错则返回-1。

  • 对于IPv4,domain参数指定为AF_INET。
  • 对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。
  • 如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。
  • protocol参数的介绍从略,指定为0即可。

参数:

  • domain:
    • AF_INET:这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址;
    • AF_INET6:与上面类似,不过是来用IPv6的地址;
    • AF_UNIX:本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用;
  • type:
    • SOCK_STREAM:这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。【只需要组应用层的数据包,底层(网络层、传输层、链路层)的包不需要人工来组,会自动组包
    • SOCK_DGRAM:这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。 【只需要组应用层的数据包,底层(网络层、传输层、链路层)的包不需要人工来组,会自动组包
    • SOCK_SEQPACKET:该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
    • SOCK_RAW:socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议) 【不仅需要组应用层的数据包,底层(网络层、传输层、链路层)的包也需要人工来组
    • SOCK_RDM:这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序
  • protocol:  
    • 传0 表示使用默认协议。
  • 返回值:
    • 成功:返回指向新创建的socket的文件描述符,
    • 失败:返回-1,设置errno

2、connect()函数 

#include <sys/types.h> 					/* See NOTES */
#include <sys/socket.h>

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

功能:连接服务器;

参数:

  • sockfd:socket的文件描述符【socket()函数的返回值】;
  • addr:指定服务器端地址信息,含IP地址和端口号【ipv4或ipv6类型的socket结构体的地址(参看第一章4.3 通用socket结构体)】;
  • addrlen:sizeof(addr)大小【ipv4或ipv6结构体的大小】
  • 返回值:成功返回0,失败返回-1,设置errno

3、bind()函数 

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

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

功能:将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。

参数

  • sockfd:socket文件描述符
  • addr:构造出IP地址加端口号【服务端的ip、端口号必须固定才能作为服务端】
  • addrlen:sizeof(addr)长度
  • 返回值:成功返回0,失败返回-1, 设置errno

服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。

bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。前面讲过,struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。如:

struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(6666);

首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为6666。

4、listen()函数

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);

参数:

  • sockfd:socket文件描述符;
  • backlog:排队建立3次握手队列和刚刚建立3次握手队列的链接数和(设置的“已完成连接队列”里的链接数与“未完成连接队列”里链接数之和的最大值,超过该最大值的连接则忽略);
    • 查看系统默认backlog:cat /proc/sys/net/ipv4/tcp_max_syn_backlog
    • [root@iZm5e9phbzdxx0lysrv9t2Z ~]# cat /proc/sys/net/ipv4/tcp_max_syn_backlog
      1024
      [root@iZm5e9phbzdxx0lysrv9t2Z ~]# 
      

典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。listen()成功返回0,失败返回-1。

5、accept()函数

#include <sys/types.h> 		/* See NOTES */
#include <sys/socket.h>

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

功能:接收监听套接字,返回可与客户端建立通信连接的新的套接字;

参数

  • sockdf:socket文件描述符;
  • addr:传入传出参数,返回获取的链接客户端地址信息,含IP地址和端口号;
  • addrlen:传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小的地址;
  • 返回值:成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno

三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。

  •  addr是一个传出参数,accept()返回时传出客户端的地址和端口号。
  • addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数传NULL,表示不关心客户端的地址。

我们的服务器程序结构是这样的:

while (1) {
	cliaddr_len = sizeof(cliaddr);
	connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
	n = read(connfd, buf, MAXLINE);
	......
	close(connfd);
}

整个是一个while死循环,每次循环处理一个客户端连接。

  • 由于cliaddr_len是传入传出参数,每次调用accept()之前应该重新赋初值。
  • accept()的参数listenfd是先前的监听文件描述符,而accept()的返回值是另外一个文件描述符connfd,之后与客户端之间就通过这个connfd通讯,
  • 最后关闭connfd断开连接,而不关闭listenfd,
  • 再次回到循环开头listenfd仍然用作accept的参数。accept()成功返回一个文件描述符,出错返回-1。

如果连接队列里没有新的连接,accept会阻塞,等待新连接的到来;

6、读数据

6.1 read()

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

ssize_t read(int sockfd, void *buf, size_t len);

6.2 recv()

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

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数:

  • flags:如果读缓冲区内数据时,不想删除缓冲区的数据,则设置flags为宏MSG_PEEK,如果设置为0则功能和read()一样;

7、写数据

7.1 write()

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

ssize_t send(int sockfd, const void *buf, size_t len);

7.2 send()

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

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数:

  • flags:
    • 如果设置为0:则功能和write()一样(flags一般设置为0);
    • 如果设置为1:则表示紧急数据(很少用);

二、TCP服务端代码

1、简易版代码

#include <stdio.h>
#include <stdlib.h>

#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
	//创建套接字
	int lfd = socket(AF_INET,SOCK_STREAM,0);
	
    //绑定
	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(8000);
	//	addr.sin_addr.s_addr = INADDR_ANY;//绑定的是通配地址
	inet_pton(AF_INET,"192.168.21.37",&addr.sin_addr.s_addr);
	int ret = bind(lfd,(struct sockaddr *)&addr,sizeof(addr));
	if(ret < 0)
	{
		perror("");
		exit(0);

	}
	
    //监听
	listen(lfd,128);
	
    //提取
	struct sockaddr_in cliaddr;
	socklen_t len = sizeof(cliaddr);
	int cfd = accept(lfd,(struct sockaddr *)&cliaddr,&len);
	char ip[16]="";
	printf("new client ip=%s port=%d\n",inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16),	ntohs(cliaddr.sin_port));
	
    //读写
	char buf[1024]="";
	while(1)
	{
		bzero(buf,sizeof(buf));
	//	int n = read(STDIN_FILENO,buf,sizeof(buf));
	//	write(cfd,buf,n);
		int n =0;
		n = read(cfd,buf,sizeof(buf));
		if(n ==0 )//如果read返回等于0,代表对方关闭 
		{
			printf("client close\n");
			break;
		}
		printf("%s\n",buf);
	
	}
	
    //关闭
	close(lfd);
	close(cfd);
	return 0;
}

2、实战版代码

上面的代码由很多缺陷,每一个步骤基本没有判断是否成功,下面是对每一个步骤进行封装的比较完善的代码(包裹函数):

wrap.h

#ifndef __WRAP_H_
#define __WRAP_H_
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>

void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);
int tcp4bind(short port,const char *IP);
#endif

wrap.c

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>

void perr_exit(const char *s)
{
	perror(s);
	exit(-1);
}

int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
	int n;

again:
	if ((n = accept(fd, sa, salenptr)) < 0) {
		if ((errno == ECONNABORTED) || (errno == EINTR))//如果是被信号中断和软件层次中断,不能退出
			goto again;
		else
			perr_exit("accept error");
	}
	return n;
}

int Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
    int n;

	if ((n = bind(fd, sa, salen)) < 0)
		perr_exit("bind error");

    return n;
}

int Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
    int n;

	if ((n = connect(fd, sa, salen)) < 0)
		perr_exit("connect error");

    return n;
}

int Listen(int fd, int backlog)
{
    int n;

	if ((n = listen(fd, backlog)) < 0)
		perr_exit("listen error");

    return n;
}

int Socket(int family, int type, int protocol)
{
	int n;

	if ((n = socket(family, type, protocol)) < 0)
		perr_exit("socket error");

	return n;
}

ssize_t Read(int fd, void *ptr, size_t nbytes)
{
	ssize_t n;

again:
	if ( (n = read(fd, ptr, nbytes)) == -1) {
		if (errno == EINTR)//如果是被信号中断,不应该退出
			goto again;
		else
			return -1;
	}
	return n;
}

ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
	ssize_t n;

again:
	if ( (n = write(fd, ptr, nbytes)) == -1) {
		if (errno == EINTR)
			goto again;
		else
			return -1;
	}
	return n;
}

int Close(int fd)
{
    int n;
	if ((n = close(fd)) == -1)
		perr_exit("close error");

    return n;
}

/*参三: 应该读取固定的字节数数据*/
ssize_t Readn(int fd, void *vptr, size_t n)
{
	size_t  nleft;              //usigned int 剩余未读取的字节数
	ssize_t nread;              //int 实际读到的字节数
	char   *ptr;

	ptr = vptr;
	nleft = n;

	while (nleft > 0) {
		if ((nread = read(fd, ptr, nleft)) < 0) {
			if (errno == EINTR)
				nread = 0;
			else
				return -1;
		} else if (nread == 0)
			break;

		nleft -= nread;
		ptr += nread;
	}
	return n - nleft;
}
/*:固定的字节数数据*/
ssize_t Writen(int fd, const void *vptr, size_t n)
{
	size_t nleft;
	ssize_t nwritten;
	const char *ptr;

	ptr = vptr;
	nleft = n;
	while (nleft > 0) {
		if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
			if (nwritten < 0 && errno == EINTR)
				nwritten = 0;
			else
				return -1;
		}

		nleft -= nwritten;
		ptr += nwritten;
	}
	return n;
}

static ssize_t my_read(int fd, char *ptr)
{
	static int read_cnt;
	static char *read_ptr;
	static char read_buf[100];

	if (read_cnt <= 0) {
again:
		if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
			if (errno == EINTR)
				goto again;
			return -1;
		} else if (read_cnt == 0)
			return 0;
		read_ptr = read_buf;
	}
	read_cnt--;
	*ptr = *read_ptr++;

	return 1;
}

ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
	ssize_t n, rc;
	char    c, *ptr;

	ptr = vptr;
	for (n = 1; n < maxlen; n++) {
		if ( (rc = my_read(fd, &c)) == 1) {
			*ptr++ = c;
			if (c  == '\n')
				break;
		} else if (rc == 0) {
			*ptr = 0;
			return n - 1;
		} else
			return -1;
	}
	*ptr  = 0;

	return n;
}

int tcp4bind(short port,const char *IP)
{
    struct sockaddr_in serv_addr;
    int lfd = Socket(AF_INET,SOCK_STREAM,0);
    bzero(&serv_addr,sizeof(serv_addr));
    if(IP == NULL){
        //如果这样使用 0.0.0.0,任意ip将可以连接
        serv_addr.sin_addr.s_addr = INADDR_ANY;
    }else{
        if(inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr) <= 0){
            perror(IP);//转换失败
            exit(1);
        }
    }
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port   = htons(port);
   // int opt = 1;
	//setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    Bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
    return lfd;
}