TCP网络编程流程


        TCP网络编程是目前比较通用的方式,例如HTTP协议、FTP协议等很多广泛应用的协议均基于TCP协议。TCP编程主要为C/S模式,即客户端(C)、服务器(S)模式,这两种模式之间的程序设计流程存在很大的差别。

TCP网络编程架构

        TCP网络编程有两种模式,一种是服务器模式,另一种是客户端模式。服务器模式创建一个服务程序,等待客户端用户的连接,接收到用户的连接请求后,根据用户的请求进行处理;客户端模式则根据目的服务器的地址和端口进行连接,向服务器发送请求并对服务器的响应进行数据处理。

服务器端的程序设计模式

在这里插入图片描述

        如图所示左边为TCP连接连接的服务器模式的程序设计流程。流程主要分为套接字初始化(socket()函数),套接字与端口的绑定(bind()函数),设置服务器的侦听连接(listen()函数),接受客户端连接(accept()函数),接收和发送数据(read()函数、write()函数)并进行数据处理及处理完毕的套接字关闭(close()函数)。

  • 套接字初始化过程中,根据用户对套接字的需求来确定套接字的选项。这个过程中的函数为socket(),它按照用户定义的网络类型、协议类型和具体的协议标号等参数来定义。系统根据用户的需求生成一个套接字描述符供用户使用。
  • 套接字与端口的绑定过程中,将套接字与一个地址结构进行绑定。绑定之后,在进行网络程序设计的时候,套接字所代表的IP地址和端口地址,以及协议类型等参数按照绑定值进行操作。
  • 由于一个服务器需要满足多个客户端的连接请求,而服务器在某个时间仅能处理有限个数的客户端连接请求,所以服务器需要设置服务器端排队队列的长度。服务器侦听连接会设置这个参数,限制客户端中等待服务器处理连接请求的队列长度。
  • 在客户端发送连接请求之后,服务器需要接受客户端的连接,然后才能进行其他的处理。
  • 在服务器接受客户端请求之后,可以从套接字文件描述符中读取数据或者向文件描述符发送数据。接受数据后服务器按照定义的规则对数据进行处理,并将结构发送给客户端。
  • 当服务器处理完数据,要结束与客户端的通信过程的时候,需要关闭套接字连接。
客户端的程序设计模式

        上图右边为客户端模式,主要分为套接字初始化(socket()函数),连接服务器(connect()函数),读写网络数据(read()函数、write()函数),并进行数据处理和最后的套接字关闭(close()函数)过程。
        客户端程序设计模式流程与服务器的处理模式流程类似,二者的不同之处是客户端在套接字初始化后可以不进行地址绑定,而是直接连接服务器端。
        客户端连接服务器的处理过程中,客户端根据用户设置的服务器地址、端口等参数与特定的服务器程序进行通信。

客户端与服务器的交互过程

客户端与服务器在连接、读写数据、关闭过程中有交互过程。

  • 客户端在连接过程,对服务器端是接受过程,在这个过程中客户端与服务器进行三次握手,建立TCP连接。在建立TCP连接之后,客户端与服务器之间可以进行数据交互。
  • 客户端与服务器之间的数据交互是相对的过程,客户端的读数据过程对应了服务器端的写数据过程,客户端的写数据过程对应服务器端的读数据过程。
  • 在服务器和客户端之间的数据交互完毕之后,关闭套接字连接。

创建网络插口函数socket()

        网络程序设计中的套接字系统调用socket()函数用来获得文件描述符。

socket()函数介绍

        socket()函数的原型如下,这个函数建立一个协议族为domain、协议类型为type、协议编号为protocol的套接字文件描述符。如果函数调用成功,会返回一个表示套接字的文件描述符,失败的时候返回-1。

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

        函数socket()的参数domain用于设置网络通信的域,函数socket()根据这个参数选择通信协议的族。通信协议族在文件sys/socket.h中定义,以太网中应该使用PF_INET这个域。在程序设计的时候会发现有的代码使用了AF_INET这个值,在头文件中AF_INET和PF_INET的值是一致的。
在这里插入图片描述
        函数socket()的参数type用于设置套接字通信的类型,如下表所示,type格式定义值及含义。主要有SOCK_STREAM(流式套接字)、SOCK_DGRAM(数据包套接字)等。
在这里插入图片描述
        并不是所有的协议族都实现了这些协议类型,例如,AF_INET协议族就没有实现SOCK_SEQPACKET协议类型。
        函数socket()的第3个参数protocol用于指定某个协议的特定类型,即type类型中的某个类型。通常某个协议中只有一个特定类型,这样protocol参数仅能设置为0;但是有些协议有多种特定的类型,就需要设置这个参数来选择特定的类型。

  • 类型为SOCK_STREAM的套接字表示一个双向的字节流,与管道类似。流式的套接字在进行数据收发之前必须已经连接,连接使用connect()函数进行。一旦连接, 可以使用read()或者write()函数进行数据的传输。流式通信方式保证数据不会丢失或者重复接受,当数据在一段时间内仍然没有接受完毕,可认为这个连接已经死掉。
  • SOCK_DGRAM和SOCK_RAW这两种套接字可以使用函数sendto()来发送数据,使用recvfrom()函数接受数据,recvfrom()接受来自指定IP地址的发送方的数据。
  • SOCK_PACKET是一种专用的数据包,它直接从设备驱动接受数据。

        函数socket()并不总是执行成功,有可能会出现错误,错误的产生有多种原因,可以通过errno获得,具体值和含义在下表中列出。通常情况下造成函数socket()失败的原因是输入的参数错误造成的,例如某个协议不存在等,这时需要详细检查函数的输入参数。由于函数的调用不一定成功,在进行程序设计的时候,一定要检查返回值。

在这里插入图片描述
        使用socket()函数的时候需要设置上述3个参数,如将socket()函数的第1个参数domain设置为AF_INET,第2个参数设置为SOCK_STREAM,第3个参数设置为0,建立一个流式套接字。

int sock = socket(AF_INET, SOCK_STREAM, 0);
应用层函数socket()和内核函数之间的关系

        用户设置套接字的参数后,函数要能够起作用,需要与内核空间的相关系统调用交互。应用层的socket()函数是和内核层的系统调用相对应的,如下图所示。
在这里插入图片描述
        图中用户调用函数sock = socket(AF_INET, SOCK_STREAM, 0),这个函数会调用系统调用函数sys_socket(AF_INET, SOCK_STREAM, 0)(在文件net/socket.c中)。系统调用函数sys/socket()分为两部分,一部分生成内核socket结构(注意与应用层的socket()函数是不同的),另一部分与文件描述符绑定,将绑定的文件描述符值传给应用层。内核sock结构如下(在linux/net.h中):

struct socket{
	socket_state	       state;	//socket状态(例如SS_CONNECTED等)
	unsigned long	       flags;	//socket标志(SOCK_ASYNC_NOSPACE等)
	const struct proco_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结构。

绑定一个地址端口对bind()函数

        在建立套接字文件描述符成功后,需要对套接字进行地址和端口的绑定,才能进行数据的接收和发送操作。

bind()函数介绍

        bind()函数将长度addlen的struct sockadd类型的参数my_addr与sockfd绑定在一起,将sockfd绑定到某个端口上,如果使用connect()函数则没有绑定的必要。绑定的函数原型如下:

#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);

        bind()函数有三个参数,第一个参数sockfd是用socket()函数创建的文件描述符。
        第二个参数my_addr是指向一个结构为sockaddr参数的指针,sockaddr包含了地址、端口和IP地址的信息。在进行地址绑定的时候,需要先将地址结构中的IP地址、端口、类型等结构struct sockaddr中的域进行设置后才能进行绑定,这样进行绑定后才能将套接字文件描述符与地址等结合在一起。
        第三个参数addrlen是my_addr结构的长度,可以设置成sizeof(struct sockaddr)。使用sizeof(struct sockaddr)来设置addlen是一个良好的习惯,虽然一般情况下使用AF_INET来设置套接字的类型和其对应的结构,但是不同类型的套接字有不同的地址描述符,如果对地址长度进行了强制的指定,可能会造成不可预料的结果。
        bind()函数的返回值为0时表示绑定成功 ,-1表示绑定失败,errno的错误值如下表所示:

在这里插入图片描述
        下面的代码初始化一个AF_UINIX族中的SOCK_STREAM类型的套接字。先使用结构struct sockaddr_un初始化my_addr,然后进行绑定,结构struct sockaddr_un的定义如下:

struct sockaddr_un{
	sa_family_t sun_family;	//协议族,应该设置为AF_UNIX
	char sun_path[UNIX_PATH_MAX];	//路径名,UNIX_PATH_MAX的值为108
};
bind()函数的例子

        下面是使用bind()函数进行程序设计的一个实例代码,先建立一个UNIX族的流类型套接字,然后将套接字地址和套接字文件描述符进行绑定。

#define MY_SOCK_PATH "/somepath"
int main(int argc, char *argv[])
{
	int sfa;
	struct sockaddr_un addr;	//AF_UNIX对应的结构
	sfd = socket(AF_UNIX, SOCK_STREAM, 0);	//初始化一个AF_UNIX族的流类型socket
	
	if(sfd == -1)	//检查是否正常初始化socket
	{
		perror("socket");
		exit(EXIT_FALIURE);
	}
	memest(&addr, 0, sizeof(struct sockaddr_un);	//将变量addr置0
	addr.sun_family = AF_UNIX;	//协议族为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_FALIURE);
		}
		···	//数据接收发送及处理过程
		close(sfd);	//关闭套接字文件描述符
}
  • 第5行将协议族参数设置为AF_UNIX建立UNIX族套接字,使用函数socket()进行建立。
  • 第10行初始化地址结构,将UNIX地址结构设置为0,这是进行程序设计时常用的初始方法。
  • 第11行将地址结构的参数sun_family设置为AF_UNIX。
  • 将12行复制地址结构的路径
  • 第14行将套接字文件描述符与UNIX地址结构进行绑定
  • 第15行判断是否绑定成功
  • 第19行进行数据的接收,发送和数据的处理过程

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 argc, char *argv[])
{
	int sockfd;	//套接字文件描述符变量
	struct sockaddr_in my_addr;	//以太网套接字地址结构

	sockfd = socket(AF_INET, SOCK_STREAM, 0);	//初始化socket
	if(sockfd == -1){	//检查是否正常初始化socket
		perror("socket");
		eixt(EXIT_FAILURE);
	}
	my_addr.sin_family = AF_INET;	//地址结构的协议族
	my_addr.sin_port = htons(MYPORT);	//地址结构的端口地址,网络字节序
	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 *)&my_addr,
		sizeof(struct sockaddr)) == -1){	//判断是否绑定成功
		perror("bind");
		eixt(EXIT_FAILURE);
	}
}
  • 第1行定义地址结构中需要绑定地址的端口值
  • 第5行和第6行初始化套接字文件描述符和以太网地址结构的变量
  • 第8行建立一个AF_INET类型的流式套接字
  • 第9-12行是套接字初始化失败的处理措施
  • 第13行设置地址结构的协议族为AF_INET
  • 第14行设置地址结构的端口地址为MYPORT,由于MYPORT为主机字节序,使用函数htons()进行字节序转换。
  • 第15行设置地址结构的IP地址,使用函数inet_addr()将字符串192.168.1.150转换为二进制网络字节序的IP地址值
  • 第16行将地址结构的sin_zero域设置为0
  • 第17行将地址结构与套接字 文件描述符进行绑定
  • 第22行接收数据、发送数据和进行数据处理的过程
  • 第23行在数据处理过程结束后,关闭套接字
应用层bind()函数和内核函数之间的关系

函数bind()是应用层函数,要使函数生效,就要将相关的参数传递给内核并进行处理。应用层的bind()函数与内核层之间的函数过程如图所示,图中是一个AF_INET族函数进行绑定的调用过程。
在这里插入图片描述
        应用层的函数bing(sockfd,(struct sockaddr*)&my_addr,sizeof(struct sockaddr))调用系统函数过程sys_bind(sockfd,(struct sockaddr*)&my_addr,sizeof(struct sockaddr))。sys_bind()函数首先调用函数sockfd_lookup_light()来获得文件描述符sockfd对应的内核struct sock结构变量,然后调用函数move_addr_to_kernel()将应用层的参数my_addr复制进内核,放到address变量中。
        内核的sock结构是在调用sock()函数时根据协议生成的,它绑定了不同协议族的bind()函数的实现方法,在AF_INET族中的实现函数为inet_bind(),即它会调用AF_INET族的bind()函数进行绑定处理。

监听本地端口listen

        服务器模式中有listen()和accept()两个函数,而客户端则不需要这两个函数。函数listen()用来初始化服务器可连接队列,服务器处理客户端连接请求的时候是顺序处理的,同一时间仅能处理一个客户端连接。当多个客户端的请求连接请求同时到来的时候,服务器并不是同时处理,而是将不能处理的客户端连接请求放到等待队列中,这个队列的长度由listen()函数来定义。

listen()函数介绍

listen()函数的原型如下, 其中的backlog表示等待队列的长度

#include <sys/socket.h>
int listen(int sockfd, int backlog);

        当listen()函数成功运行时,返回值为0;当运行失败时,它的返回值为-1,并且设置errno值,错误代码的含义如表所示。
在这里插入图片描述
        在接收一个连接之前,需要用listen()函数来侦听端口,listen()函数中参数backlog的参数表示在accept()函数处理之前在等待队列中的客户端的长度,如果超过这个长度,客户端会返回一个ECONNREFUSED错误。
        listen()函数仅对类型为SOCK_STREAM或者SOCK_SEQPACKET的协议有效,例如,如果对一个SOCK_DGRAM的协议使用函数listen(),将会出现错误errno应该为值EOPNOTSUPP,表示此socket不支持函数listen()操作。大多数系统的设置为 20,可以将其设置修改为5或者10,根据系统可承受负载或者应用程序的需求来确定。

listen()函数的例子

        下面是一个listen()函数的实例代码,在成功进行socket()函数初始化和bind()函数端口之后,设置listen()函数队列的长度为5。

#define MYPORT 3490	//端口地址
int main(int argc, char *argv[])
{
	int sockfd;	//套接字文件描述符变量
	struct sockaddr_in my_addr;	//以太网套接字地址结构

	sockfd = socket(AF_INET, SOCK_STREAM, 0);	//初始化socket
	if(socket == -1){	//检查是否正常初始化socket
		perror("socket");
		exit(EXIT_FAILURE);
	}
	my_addr.sin_family = AF_INET;	//地址结构的协议族
	my_addr.sin_port = htons(MYPORT);	//地址结构的端口地址,网络字节序
	my_addr.sin_addr.s_addr = inet_addr("192.168.1.150");	//IP,将字符串的IP地址转化为网络字节序
	bzero(&(my_addr.sin_zero), 0);	//将my_addr.sin_zero置0
	if(bind(sockfd,  (struct sockaddr *)&my_addr,
		sizeof(struct sockaddr)) == -1){	//判断是否绑定成功
			perror("bind");	//打印错误信息
			exit(EXIT_FAILURE);	//退出程序
		}
	if(listen(sockfd,5) == -1)	//进行侦听队列长度的绑定,判断是否listen成功
	{
		perror("listen");
		exit(EXIT_FAILURE);
	}
	···
	close(sockfd);

}
  • 第1行定义了地址结构中需要绑定地址的端口值
  • 第5行和第6行初始化套接字文件描述符和以太网地址结构的变量
  • 第8行建立一个AF_INET类型的流式套接字
  • 第9-12行如果套接字初始化失败的时候处理措施
  • 第13行设置地址结构的协议族为AF_INET
  • 第14行设置地址结构的端口地址MYPORT,由于MYPORT为主机字节序,使用函数htons()进行字节序转换。
  • 第15行设置地址结构的IP地址,使用函数inet_addr()将字符串192.168.1.150转换为二进制网络字节序的IP地址值
  • 第16行将地址结构的sin_zero域设置为0
  • 第17行将地址结构与套接字文件描述符进行绑定
  • 第22行设置套接字sockfd的侦听队列长度为5
  • 第27行接收收据、发送数据和进行数据处理的过程
  • 第28行在数据处理过程结束后,关闭套接字
应用层listen()函数和内核函数之间的关系

        应用层listen()函数和内核层listen()函数的关系如图所示,应用层的listen()函数对应于系统调用sys_listen()函数。sys_listen()函数首先调用sockfd_lookup_light()函数获得sockfd对应的内核结构struct socket,查看用户的backlog设置值是否过大,如果过大则设置为系统默认最大 设置。然后调用抽象的listen()函数,这里指的是AF_INET的listen()函数和inet_listen()函数。
        inet_listen()函数首先判断是否合法的协议族和协议类型,再更新socket的状态值为TCP_LISTEN,然后为客户端的等待队列申请空间并设定侦听端口。

接收一个网络请求accept()函数

        当一个客户端的连接请求到达服务器主机侦听的端口时,此时客户端的连接会在队列中等待,直到使用服务器处理接收请求。
        函数accept()成功执行后,会返回一个新的套接字文件描述符来表示客户端的连接,客户端连接的信息可以通过这个新的文件描述符来获得。因此当服务器成功处理客户端的请求连接后,会有两个文件描述符,老的文件描述符表示正在监听的socket,新产生的文件描述符表示客户端的连接,函数send()和recv()通过新的文件描述符进行数据收发。

accept()函数介绍

accept()函数的原型如下:

#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

        通过accept()函数可以得到成功连接客户端的IP地址、端口和协议族等信息,这个信息是通过参数addr获得的。当accept()函数返回的时候,会将客户端的信息存储在参数addr中,参数addrlen表示第2个参数(addr)所指内容的长度,可以使用sizeof(struct sockaddr_in)来获得。需要注意的是,在accept中addrlen参数是一个指针而不是结构,accept()函数将这个指针传给TCP/IP协议栈。
        accept()函数的返回值是新连接的客户端套接字文件描述符,与客户端之间的通信是通过accept()函数返回的新套接字文件描述符来进行的,而不是建立套接字时的文件描述符,这是在程序设计的时候需要注意的地方。如果accept()函数发生错误,accept()函数会返回-1。通过errno可以得到错误值,含义如下表介绍。
在这里插入图片描述

accept()函数的例子

        下面是一个简单的使用accept()函数的例子。这个例子先建立一个流式套接字,然后对套接字进行地址绑定,当绑定成功后,初始化侦听队列的长度,然后等待客户端的连接请求。

int main(int argc, char *argv[])
{
	int sockfd, client_fd;	//sockfd为侦听的socket,client_fd为连接方的socket值

	struct sockaddr_in my_addr;	//本地地址信息
	struct sockaddr_in client_addr;	//客户端连接的地址信息
	int addr_length;	//int类型变量,用于保存网络地址长度量
	sockfd = socket(AF_INET,SOCK_STREAM, 0);	//初始化一个IPV4族的流式连接
	if(sockfd == -1){	//检查是否正常初始化socket

		perror("socket");
		exit(EXIT_FAILURE);
	}
	my_addr.sin_family = AF_INET;	//协议族为IPV4,主机字节序
	my_addr.sin_port = htons(MYPORT);	//端口,短整型,网络字节序
	my_addr.sin_addr.s_addr = INADDR_ANY;	//自动IP地址获得
	bzero(&(my_addr.sin_zero), 8);	//置0
	if(bind(sockfd,  (struct sockaddr *)&my_addr,	//绑定端口地址
		sizeof(struct sockaddr)) == -1){
			perror("bind");
			eixt(EXIT_FAILURE);
	}
	if(listen(sockfd,BACKLOG) == -1)	//设置侦听队列长度为BACKLOG = 10
	{
		perror("listen");
		eixt(EXIT_FAILURE);
	}
	addr_length =  sizeof(struct sockaddr_in);	//地址长度
	client_fd = accept(sockfd, &client_addr, &addr_length);	//等待客户端连接,地址在client_addr中
	if(client_fd == -1){	//accept出错
		perror("accept")
		exit(EXIT_FAILURE);
	}
	···	//处理客户端连接过程
	close(client_fd);	//关闭客户端连接
	···
	close(sockfd);	//关闭服务器连接
}
  • 第3行定义了服务器套接字变量sockfd和客户端连接时新产生的套接字变量client_fd.
  • 第4行和第5行定义本地地址结构变量my_addr和客户端连接时产生的地址结构变量client_addr。
  • 第6行定义地址结构的长度变量addr_length,在accept()函数调用的时候用于传入地址结构的长度
  • 第7-11行初始化一个IPV4族的流式连接并进行错误处理
  • 第12-14行进行服务器地址结构的初始化,其中本地IP地址设置为INADDR_ANY,表示任意的本地IP地址
  • 第16行将地址结构的sin_zero域设置为0
  • 第16-20行将服务器地址结构与套接字进行绑定并进行错误处理
  • 第21-25行设置侦听队列的长度并进行错误处理
  • 第26行获取地址结构的长度
  • 第27-31行接收客户端的连接并进行错误处理。先等待客户端的连接,客户端连接成功后,client_fd为客户端的套接字地址。
  • 第33行省略的代码是通过套接字文件描述符client_fd处理客户端的数据传输
  • 第34行在对客户端的数据处理过程结束后,关闭客户端套接字。
  • 第35行省略的代码是使服务器的其他过程,例如接收新的客户端连接等操作。
  • 第36行当服务器需要关闭的时候,使用close(sockfd)进行服务器套接字的关闭。
应用层accept()函数和内核函数之间的关系

        应用层的accept()函数和内核层的accept()函数如下图所示。应用层的accept()函数对应内核层的sys_accept()函数系统调用函数。函数sys_accept()查找文件描述符对应的内核socket结构、申请一个用于保存客户端连接的新的内核socket结构、获得客户端的地址信息、将连接的客户端地址信息复制到应用层的用户、返回连接客户端socket对应的文件描述符。
在这里插入图片描述

        函数sys_accept()调用函数sockfd_lookup_light()查找到文件描述符对应的内核socket结构后,然后会申请到一块内存用于保存连接成功的客户端的状态。socket结构的一些参数,如类型type、操作方式ops等会继承服务器原来的值,假如原来服务器的类型为AF_INET,则其操作模式仍然是af_inet.c文件中的各个函数。然后会查找文件描述符表,获得一个新结构对应的文件描述符。
        accept()函数的实际调用根据协议族的不同而不同,即函数指针sock->ops->accept要由socket()函数初始化协议族而确定。当为AF_INET时,此函数指针对应于af_inet.c文件中的inet_accept()函数。
        当客户端连接成功后,内核准备连接的客户端的相关信息,包含客户端的IP地址、客户端的端口等信息,协议族的值继承原服务器的值。在成功获得信息之后会调用move_addr_to_user()函数将信息复制到应用层空间,具体的地址由用户传入的参数来确定。

连接目标网络服务器connect()函数

        客户端在建立套接字之后,不需要进行地址绑定就可以直接连接服务器。连接服务器的函数为connect(),此函数连接指定参数的服务器,例如IP地址、端口等。

connect()函数介绍

        connect()函数的原型如下,其中的参数sockfd是建立套接字时返回的套接字文件描述符,它是由系统调用socket()函数返回的。参数serv_addr是一个指向数据结构sockaddr的指针,其中包括客户端需要连接的服务器的目的端口和IP地址,以及协议类型。参数addrlen表示第二个参数内容的大小,可以使用sizeof(struct sockaddr)而获得,与bind()函数不同,这个参数是一个整型的变量而不是指针。

#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *, int addrlen);

        connect()函数的返回值在成功时返回0,当发生错误的时候返回-1,可以查看errno获得错误的原因。错误值及含义在如下所示。
在这里插入图片描述

connect()函数的例子

        下面是connect()函数的使用实例代码,与服务器的代码类似,先建立一个套接字文件描述符,当成功建立描述符后,将需要连接的服务器IP地址和端口填充到一个地址结构中,connect()函数连接到地址结构所指定的服务器上。

#define DEST_IP "132.241.5.10"	//服务器的IP地址
#define DEST_PORT 23	//服务器端口
int main(int argc, char *argv[])
{
	int ret = 0;
	int sockfd;	//sockfd为连接的socket
	struct sockaddr_in server;	//服务器地址的信息
	sockfd = socket(AF_INET, SOCK_STREAM, 0);	//初始化一个IPV4族的流式连接
	if(sockfd == -1)	//检查是否正常初始化socket
	{
		perror("socket");
		exit(EXIT_FAILURE);
	}
	server.sin_family = AF.INET;	//协议族的IPV4,主机字节序
	server.sin_port = htons(DEST_PORT);	//端口,短整型,网络字节序
	server.sin_addr.s_addr = htonl(DEST_IP);	//服务器的IP地址
	bzero(&(server.sin_zero), 0);	//保留字段置0
	
	ret = connect(sockfd, (struct sockaddr *)& server, sizeof(struct sockaddr));	//连接服务器
	···	//接收或者发送数据
	close(sockfd);
}
  • 第1行和第2行分别定义了客户端连接的服务器IP地址和服务器的侦听端口
  • 第8-12行建立一个AF_INET类型的流式套接字并进行错误处理
  • 第13行和第16行对以太网结构的变量的参数进行赋值
  • 第18行连接服务器
  • 第19行省略部分的代码表示接收数据、发送数据和进行数据处理的过程
  • 第20行在数据处理过程结束后,关闭套接字。
应用层connect()函数和内核函数之间的关系

        应用层connect()函数和内核层的connect()函数关系如图所示,内核曾的connect()函数比较简单,主要进行不同的协议映射的时候要根据协议的类型进行选择,例如数据报和流式数据的connect()函数不同,流式的回调函数为inet_stream_connect(),数据报的回调函数为inet_dgram_connect()。
在这里插入图片描述

写入数据函数write()

        如上图所示,当服务器端在接收到一个客户端的连接后,可以通过套接字描述符进行数据的写入操作。对套接字进行写入的形式和过程与普通文件的操作方式一致,内核会根据文件描述符的值来查找所对应的属性,当为套接字的时候,会调用相对应的内核函数。
        下面是一个向套接字文件描述符中写入数据的例子,将缓冲区data的数据全部写入套接字文件描述符s中,返回值为成功写入的数据长度。

int size;
char data[1024];
size = write(s,data,1024);

读取数据函数read()

        与写入数据类似,使用read()函数可以从套接字描述符中读取数据。当然,在读取数据之前,必须建立套接字并连接。读取数据的方式如下所示,从套接字描述符s中读取1024个字节,放入缓冲区data中,size变量的值为成功读取的数据大小。

int size;
char data[1024];
size = read(s, data, 1024);

关闭套接字函数

        关闭socket连接可以使用close()函数实现,函数的作用是关闭已经打开socket连接,内核会释放相关的资源,关闭套接字之后就不能再使用这个套接字文件描述符进行读写操作了。
        函数shutdown()可以使用更多方式来关闭连接,允许但单方向切断通信或者切断双方的通信。函数原型如下,第一个参数s是切断通信的套接口文件描述符,第二个参数how表示切断的方式。

#include <sys/socket.h>
int shutdown(int s, int how);

        函数shutdown()用于关闭双向连接的一部分,具体的关闭行为方式通过参数的how设置来实现,可以为如下值:

  • SHUT_RD:值为0,表示切断读,之后不能使用此文件描述符进行读操作
  • SHUT_WR:值为1,表示切断写,之后不能使用此文件描述符进行写操作
  • SHUT_RDWR:值为2,表示切断读写,之后不能使用此文件描述符进行读写操作,与close()函数功能相同。

        函数shutdown()如果调用成功则返回0,如果失败则返回-1,通过errno可以获得错误的具体信息,错误值含义参见表
在这里插入图片描述

服务器端简单例子

1. socket
2. bind
3. listen
4. accept
5. read
6. write
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
int main()
{
	int s_fd;
	//1.socket
	s_fd = socket(AF_INET,SOCK_STREAM,0);
	if(s_fd == -1){
		perror("socket");
		exit(-1);
	}
	
	//配置信息
	struct sockaddr_in s_addr;
	s_addr.sin_family = AF_INET;
	s_addr.sin_port = htons(8989);
	inet_aton("127.0.0.1",&s_addr.sin_addr);
	
	//2.bind
	bind(s_fd,(struct sockaddr *)&s_addr, sizeof(struct sockaddr_in));

	//3.listen
	listen(s_fd,10);

	//4.accept
	int c_fd = accept(s_fd,NULL,NULL);
	
	if(c_fd > 0)
	{
		printf("connected\n");
	} 	

	//5.read

	//6.write
	
	return 0;
}

可以在主系统cmd窗口上用telnet指令来连接服务器端
在这里插入图片描述
在这里插入图片描述

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LEO-max

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值