欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

chapter04_基本TCP套接字编程

程序员文章站 2022-03-22 23:21:45
...

chapter_04 传输层:基本TCP套接字编程

这篇文章是我自己看的。阅读《unix网络编程》,整理自己的思路,仅放在网上便于保存&&分享。

相对于书而言,内容没有什么价值。书上的内容全面可靠。

PS:文章是建立在我的知识体系之上。因而,文中也不会有多余解释。(如果有不清楚的阅读书本/google)

必要的内容放在正文中。相对而言非主线的内容放在附录中。这两个集合之外的内容,可能没有写出,或者随它而去。

为了好看增加颜色标识。提问内容颜色标识:绿色; 重点程度颜色标识:红色>蓝色>黑体。

(csdn自身的颜色不在考虑范围内。)


一、内容

1、概述

本章讲解编写一个完整的TCP客户/服务器程序所需要的基本套接字函数。



2、基本套接字函数

整体的代码,见代码部分。

2.1、socket函数

函数原型: int socket(int family, int type, int protocol);

eg:int sockfd = socket(AF_INET, SOCK_STREAM, 0)

函数作用:指定期望的通信协议类型

参数解析:

  • family参数指明协议族
  • type参数指明套接字类型
  • protocol参数应设为某个协议类型常值, 或者设为0, 以选择所给定family和type组合的系统默认值。

参数表:

其中标为“是”的项也是有效的, 但还没有找到便捷的缩略词。 而空白项则是无效组合。

chapter04_基本TCP套接字编程

返回值:

socket函数在成功时返回一个小的非负整数值, 它与文件描述符类似, 我们把它称为套接字描述符(socket descriptor) , 简称sockfd。


2.2 、connect函数

函数原型:int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

eg:connect(sockfd, (SA *) &servaddr, sizeof(servaddr))

函数作用:TCP客户用connect函数来建立与TCP服务器的连接。

参数解析:

  • sockfd是由socket函数返回的套接字描述符
  • 第二个、 第三个参数分别是一个指向套接字地址结构的指针和该结构的大小

函数返回:若成功则为0, 若出错则为-1

出错原因:

  • 若TCP客户没有收到SYN分节的响应, 则返回ETIMEDOUT错误。
  • 若对客户的SYN的响应是RST(表示复位) , 则表明该服务器主机在我们指定的端口上没有进程在等待与之连接(例如服务器进程也许没在运行) 。 这是一种硬错误(hard error) , 客户一接收到RST就马上返回ECONNREFUSED错误。
  • 若客户发出的SYN在中间的某个路由器上引发了一个“destinationunreachable”(目的地不可达) ICMP错误, 则认为是一种软错误(softerror) 。 客户主机内核保存该消息, 并按第一种情况中所述的时间间隔继续发送SYN。 若在某个规定的时间(4.4BSD规定75s) 后仍未收到响应,则把保存的消息(即ICMP错误) 作为EHOSTUNREACH或ENETUNREACH错误返回给进程

2.3、bind函数

函数原型:int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

eg:Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

函数作用:把一个本地协议地址赋予一个套接字。

函数作用解析:

  • servaddr是存储着协议地址;listenfd是套接字描述符,由socket产生;

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

  • 进程可以把一个特定的IP地址捆绑到它的套接字上, 不过这个IP地址必须属于其所在主机的网络接口之一。 对于TCP客户, 这就为在该套接字上发送的IP数据报指派了源IP地址。 对于TCP服务器, 这就限定该套接字只接收那些目的地为这个IP地址的客户连接(即监听绑定的这个ip:port)。 TCP客户通常不把IP地址捆绑到它的套接字上。 当连接套接字时, 内核将根据所用外出网络接口来选择源IP地址, 而所用外出接口则取决于到达服务器所需的路径(TCPv2第737页) 。 如果TCP服务器没有把IP地址捆绑到它的套接字上, 内核就把客户发送的SYN的目的IP地址作为服务器的源IP地址(TCPv2第943页) 。

  • 也就是说

    客户端套接字:未使用bind
    套接字里面已经含有IP/端口信息了。但信息是对方服务器的信息。我本身并不需要知道,信息从哪个端口出去。
    服务器套接字:使用bind
    得套接字里面的IP=INADDR_ANY=0 ,如果有几张网卡,这几张网卡的IP都算在内。端口号为固定(假设是9999)。这个端口是监听端口,得固定。

函数参数:

  • sockfd是由socket函数返回的套接字描述符
  • 第二个、 第三个参数分别是一个指向套接字地址结构的指针和该结构的大小

函数返回:若成功则为0, 若出错则为-1


2.4 、listen函数

函数原型:int listen(int sockfd, int backlog);

eg:Listen(listenfd, LISTENQ);

函数作用: listen函数把一个未连接的套接字转换成一个被动套接字, 指示内核应接受指向该套接字的连接请求。

函数参数:

  • sockfd是由socket函数返回的套接字描述符

  • 函数的第二个参数规定了内核应该为相应套接字排队的最大连接个数=未完成连接队列+已完成连接队列。

    黑客编写了一个以高速率给受害主机发送SYN的程序, 用以装填一个或多个TCP端口的未完成连接队列。 这样, 通过以伪造的SYN装
    填未完成连接队列, 使合法的SYN排不上队, 导致针对合法客户的服务被
    拒绝(denial of service)

函数返回:若成功则为0, 若出错则为-1


2.5 、accept函数

函数原型:int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

eg:connfd = accept(listenfd, (SA *) NULL, NULL);

函数作用: 用于从已完成连接队列队头返回下一个已完成连接.

函数参数:

  • 第一个参数为监听套接字(listening socket) 描述符(由socket创建, 随后用作
    bind和listen的第一个参数的描述符)
  • 函数的第二个参数可以用来返回客户端的协议地址;第三个参数,返回该协议地址的长度。

函数返回:若成功则为非负描述符我们称它为已连接套接字(connected socket) 描述符。 若出错则为-1


2.6 fork和exec函数

函数原型:pid_t fork(void);

函数作用:该函数是Unix中派生新进程的方法。

函数返回:在子进程中为0, 在父进程中为子进程ID, 若出错则为-1

fork有两个典型用法:

  • 一个进程创建一个自身的副本, 这样每个副本都可以在另一个副本执行其他任务的同时处理各自的某个操作。 这是网络服务器的典型用法。
  • 一个进程想要执行另一个程序。 既然创建新进程的唯一办法是调用fork, 该进程于是首先调用fork创建一个自身的副本, 然后其中一个副本(通常为子进程) 调用exec(接下去介绍) 把自身替换成新的程序。 这是诸如shell之类程序的典型用法。

2.7 close函数

函数原型:int close(int sockfd);

函数功能:关闭套接字, 并终止TCP连接

函数返回:若成功则为0, 若出错则为-1


2.8、getsockname和getpeername函数

getsockname函数原型:int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);

函数功能:返回与某个套接字关联的本地协议地址

函数参数:同上

注:在这样的调用中, 套接字描述符参数必须是已连接套接字的描述符, 而不是监听套接字的描述符

函数返回:若成功则为0, 若出错则为-1

getpeername函数原型:int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);

函数功能:返回与某个套接字关联的外地协议地址

函数参数:同上

注:该函数应该适合任何已打开的套接字描述符。

函数返回:若成功则为0, 若出错则为-1


2.9、整体过程

chapter04_基本TCP套接字编程



二、代码

在源代码的基础上进行修改。也是课后习题4.2的代码。

1、客户端代码

#include	"unp.h"

int
main(int argc, char **argv)
{
	int					sockfd, n;
	char				recvline[MAXLINE + 1];
	struct sockaddr_in	servaddr;
	struct sockaddr_in  client_addr;
	socklen_t client_addr_len=0;

	if (argc != 2)
		err_quit("usage: a.out <IPaddress>");

	if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
		err_sys("socket error");

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port   = htons(13);	/* daytime server */
	if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
		err_quit("inet_pton error for %s", argv[1]);

	if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0)
		err_sys("connect error");
	
	if(getsockname(sockfd,(SA *)&client_addr,&client_addr_len)<0) 
    	return -1;      
	printf("client address is %s:%d\n",inet_ntop(AF_INET,&client_addr.sin_addr,recvline,sizeof(client_addr)), 
	 									ntohs(client_addr.sin_port));

	// while ( (n = read(sockfd, recvline, MAXLINE)) > 0) {
	// 	recvline[n] = 0;	/* null terminate */
	// 	if (fputs(recvline, stdout) == EOF)
	// 		err_sys("fputs error");
	// }

	n=readn(sockfd,(SA *) recvline, MAXLINE + 1);
	printf("%s",recvline);

	if (n < 0)
		err_sys("read error");

	exit(0);
}

2、服务端代码

#include	"unp.h"
#include	<time.h>

int
main(int argc, char **argv)
{
	int					listenfd, connfd;
	struct sockaddr_in	servaddr;
	char				buff[MAXLINE];
	time_t				ticks;

	listenfd = Socket(AF_INET, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family      = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port        = htons(13);	/* daytime server */

	Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

	Listen(listenfd, LISTENQ);

	for ( ; ; ) {
		connfd = Accept(listenfd, (SA *) NULL, NULL);

        ticks = time(NULL);
        snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));

        //Write(connfd, buff, strlen(buff));
		writen(connfd, buff, strlen(buff));

		Close(connfd);
	}
}