TCP通信流程详解(搭配代码,案例)

一、建立套接字socket

1.套接字定义

套接字,英文名字叫做socket,是一个指向传输提供者的句柄(句柄:一种特殊的智能指针,每一个具体对象的ID,根据这个ID,操作系统可以将消息发给指定的对象),在Linux系统的网络编程中通过操作该句柄来实现网络通信和管理的。

2. 套接字分类

套接字分为三大类,分别为原始套接字(SOCK_RAW)、流式套接字(SOCKET_STREAM),流式套接字(SOCK_DGRAM)
(1)原始套接字:能够使程序开发人员对底层的网络传输机制进行控制,在原始套接字下接收的数据含有IP头。
(2)流式套接字:提供了双向、有序、可靠的数据传输,该类套接字在通信前需要双方建立连接,TCP协议采用的就是该套接字。
(3)数据包套接字:提供双向的数据流,但是它不能保证数据传输的可靠性、有序性和无重复性,UDP协议采用的就是该套接字。

这三种套接字分类在套接字创建函数中有使用到,例如:
① 如果创建的是TCP/IP协议,则应使用SOCKET_STREAM,sockfd =socket(AF_INET, SOCK_STREAM, 0);
② 如果创建的是UDP协议,则应使用SOCK_DGRAM,sockfd =socket(AF_INET, SOCK_DGRAM, 0);

3.套接字相关结构体

(1)struct sockaddr

		#include <netinet/in.h>
		struct sockaddr
		{
			unsigned short  sa_family;		/*地址族,2字节,AF_xxx*/
			char  sa_data[14];				/*14字节的协议地址*/
		}

(2)struct sockaddr_in

		#include <netinet/in.h>
		struct  sockaddr_in
		{
			short int sin_famliy;			/*地址族,2字节*/
			unsigned short  sin_port;		/*端口号,2字节,该参数填0时,表示系统随机选择一个未被使用的端口号*/
			struct in_addr  sin_addr;		/*IP地址,32位,4字节,该字节填写INADDR_ANY,表示填入本机地址*/
			unsigned char sin_zero[8];		/*填充8字节0,以保持与struct sockaddr 同样大小*/
		}

注意:
① sin_port所填写的端口号要设置为大于1024的数值,因为0~1024是保留端口号。
② 在使用该结构体时候,sin_port、sin_addr不能直接填入数值,需要将其转化为网络字节优先顺序。

(3)网络字节优先顺序
互联网上,数据以高位字节优先顺序在网络上传输,所以对于在内部是以低字节优先方式存储数据的机器,在互联网上传输数据时候,要进行转换,否则就会出现数据不一致的现象。
在socket网络编程中,有两个基本是术语是必须掌握的,网络字节顺序(NBO,Network Byte Order)和主机字节顺序(HBO,Host Network Order)。

网络字节顺序,NBO是网络数据在传输中的规定的数据格式,从高到低位顺序存储,即低字节存储在高地址,高字节存储在低地址;即“大端模式”。网络字节顺序可以避免不同主机字节顺序的差异。
主机字节顺序,HBO则与机器CPU相关,数据的存储顺序由CPU决定。

下面是在Linux系统下的几个字节顺序的转换函数

		#include <arpa/inet.h>
		uint32_t	htonl(unit32_t hostlong);		//htonl()函数把32位值从主机字节顺序转化为网络字节顺序
		uint16_t 	htons(uint16_t hostshort);		//htons把16位值从主机字节顺序转化为网络字节顺序
		uint32_t 	ntohl(uint32_t netlong);		//把32位值从网络顺序转化为主机字节顺序
		uint16_t	ntohs(uint16 netshort);			//把16位值从网络字节顺序转化成主机字节顺序
		in_addr_t 	inet_addr(“xxx.xxx.xxx.xxx”);	//将字符串式的IP地址转换为32位网络字节序的整数形式
		char 		*inet_ntoa(struct in_addr in);	//将一个32位网络字节序的二进制IP地址转换成相应的点分十进制的IP地址字符串

(4)struct sockaddr_in的使用方法

		#include <arpa/inet.h>
		#include <netinet/in.h>
		Local_addr.sin_family = AF_INET;
		Local_addr.sin_addr.s_addr =  htonl(INADDR_ANY);	//填写本机IP地址
		/*注意:如果需要填写别的IP地址,例如192.168.2.88,则需要用到inet_addr,将字符串转化为32位网络字节的整形数值*/
		/*Local_addr.sin_addr.s_addr =  inet_addr(“192.168.2.88”)*/
		Local_addr.sin_port = htons(50001);
		bzero(&(Local_addr.sin_zero),8);

(5)struct sockaddr与struct sockaddr_in的区别与联系
共同点:
① 空间大小相同,都是16字节
② 都有family属性
③ sockaddr和sockaddr_in包含的数据都是一样的

不同点:
① sockaddr用除family外的14个字节来表示sa_data,而sockaddr_in把14个字节拆分成sin_port, sin_addr和sin_zero。分别表示端口、ip地址。sin_zero用来填充字节使sockaddr_in和sockaddr保持一样大小。
② 程序员不应操作sockaddr,sockaddr是给操作系统用的程序员应使用sockaddr_in来表示地址,sockaddr_in区分了地址和端口,使用更方便。

具体使用方法:程序员把类型、ip地址、端口填充sockaddr_in结构体,然后强制转换成sockaddr,作为参数传递给调用函数。
举例:

		int sockfd;
		struct sockaddr_in servaddr;
		sockfd = Socket(AF_INET, SOCK_STREAM, 0);
		/* 填充struct sockaddr_in */
		bzero(&servaddr, sizeof(servaddr));
		servaddr.sin_family = AF_INET;
		servaddr.sin_port = htons(SERV_PORT);
		inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
		 
		/* 强制转换成struct sockaddr */
		connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr));

4.套接字创建函数socket

无论是TCP还是UDP通信,无论是作为客户端还是服务器端,建立通信的第一步都是先创建套接字。
(1)函数作用:为了创建套接字,该函数返回一个类似于文件描述符的句柄。

(2)头文件:
#include <sys/types>
#include <sys/socket.h>

(3)函数原型:int socket(int domain,int type,int protocol)

(4)函数参数:
① domain,代表所使用的协议族,通常为AF_INET,表示互联网协议族(TCP/IP协议族)
② type,指定套接字的类型:
SOCKET_STREAM(流式套接字)
SOCK_DGRAM(数据包套接字)
SOCK_RAW(原始套接字)
③ protocol,常用的有 IPPROTO_TCP 、IPPTOTO_UDP、0,分别表示 TCP 传输协议、UDP 传输协议、自动推演。

(5)protocol:一般情况下有了 domain和 type 两个参数就可以创建套接字了,填写0,操作系统会自动推演出协议类型编程中也大多使用填写0的方法。除非遇到这样的情况:有两种不同的协议支持同一种地址类型和数据传输类型。如果我们不指明使用哪种协议,操作系统是没办法自动推演的。

① 参数 domain的值为AF_INET,type选择 SOCK_STREAM ,那么满足这两个条件的协议只有 TCP,所以下面两种写法是等价的:
int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //IPPROTO_TCP表示TCP协议
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0); //0表示自动推演所对应的协议

② 参数 domain的值为AF_INET,type选择 SOCK_DGRAM,那么满足这两个条件的协议只有 UDP,所以下面两种写法是等价的:
int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); //IPPROTO_UDP表示UDP协议
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0); //0表示自动推演所对应的协议

(6)返回值:一个人int型的套接字描述符,创建城管后,这个值一般大于2,后面对套接字进行操作的函数都会调用它。-1:创建失败,返回值为0,1,2的基本属于标准输入输出套接字标识。通常用户自己创建的socket不会出现这个问题。

套接字描述符:是一个指向内部数据结构的指针,它指向描述符表入口,调用socket函数时,套接字执行体将创建一个套接字,也就是为一个套接字数据结构分配存储空间。

5.perror函数

perror函数使:perror(s) 用来将上一个函数发生错误的原因输出到标准设备(stderr)。
参数 s 所指的字符串会先打印出,后面再加上错误原因字符串。此错误原因依照全局变量errno的值来决定要输出的字符串。在库函数中有个errno变量,每个errno值对应着以字符串表示的错误类型。当你调用"某些"函数出错时,该函数已经重新设置了errno的值。perror函数只是将你输入的一些信息和现在的errno所对应的错误一起输出。

perror函数使用实例:

		#include<stdio.h>
		int main(void)
		{
		    FILE *fp;
		    fp=fopen("/root/noexitfile","r+");
		    if(NULL==fp)
		    {
		        perror("/root/noexitfile");
		    }
		    return 0;
		}

运行结果:在这里插入图片描述

二、绑定套接字bind

特别注意,bind函数是TCP服务器端才使用的,客户端无需绑定套接字。通过socket()函数调用返回一个套接字描述符后,在接收客户端连接请求以前,必须先绑定套接字,也就是将套接字与本机的IP地址和端口相关联,随后就可以在该端口下进行监听服务。

(1)头文件:
#include <sys/types.h>
#include <sys/socket.h>

(2)函数作用:把一个本地IP地址与端口号赋予一个套接字

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

(4)函数参数:
① sockfd,调用socket()函数返回的套接字描述符
② my_addr,一个指向包含本机IP地址及端口号的sockaddr指针,通常将sockaddr_in类型强转为sockaddr类型使用
③ addrlen,socklen_t和int差不多,4字节,该参数表示struct sockaddr的长度,即sizeof(struct sockaddr);

(5)返回值:绑定成功返回0;出现错误返回-1,并将errno设置成相应的错误号,即可用perror函数打印出错误信息。

三、建立连接connect函数(客户端使用)

面向连接的客户程序使用connect函数来配置套接字并与远端服务器建立一个TCP连接。言外之意,客户端才使用connect函数
connect函数是一个阻塞函数,通过TCP三次握手服务器端建立连接。客户端主动连接服务器,建立连接方式通过TCP三次握手,通知Linux内核自动完成TCP三次握手连接。如果连接成功为0 失败返回值-1。一般的情况下,客户端的connect函数默认是阻塞行为,直到三次握手阶段成功为止。
于客户端的 connect() 函数,该函数的功能为客户端主动连接服务器,建立连接是通过三次握手,而这个连接的过程是由内核完成,不是这个函数完成的,这个函数的作用仅仅是通知 Linux 内核,让 Linux 内核自动完成 TCP 三次握手连接。

(1)头文件:
#include <sys/types.h>
#include <sys/socket.h>

(2)函数作用:用于建立与指定socket的连接,连接服务器端(通知Linux内核,让内核自动完成TCP三次握手)。

(3)函数原型:int connect(int sockfd, const struct sockaddr * serv_addr, int serv_addrlen);

(4)函数参数:
① sockfd,调用socket()函数返回的套接字描述符
② serv_addr,包含远程服务器IP地址和端口号的指针
③ serv_addr,包含远程服务器IP地址和端口号的指针的长度,即sizeof(struct sockaddr)

(5)返回值: 0:成功,-1:失败,并将错误码保存至errno中,可用perror函数打印。

四、监听模式listen函数

该函数使套接字处于被动监听模式,并为该套接字建立一个输入数据队列,将达到的服务请求保存在该队列中,直到程序对他们处理。
对于服务器,它是被动连接的。举一个生活中的例子,通常的情况下,移动的客服(相当于服务器)是等待着客户(相当于客户端)电话的到来。而这个过程,需要调用listen()函数。
如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。

(1)头文件:
#include <sys/types.h>
#include <sys/socket.h>

(2)函数作用:把进程变为一个服务器,并指定相应的套接字变为被动连接。

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

(4)函数参数:
① sockfd,调用socket()函数返回的套接字描述符
② backlog,告诉内核连接队列的长度。详细介绍参考https://blog.csdn.net/kongxian2007/article/details/49153801

(5)返回值: 0:成功,-1:失败,并将错误码保存至errno中,可用perror函数打印。

注意:listen()函数不会阻塞,它主要做的事情为,将该套接字和套接字对应的连接队列长度告诉 Linux 内核,然后listen()函数就结束。这样的话,当有一个客户端主动连接(connect()),Linux 内核就自动完成TCP 三次握手,将建立好的链接自动存储到队列中,如此重复。所以,只要 TCP 服务器调用了 listen(),客户端就可以通过 connect() 和服务器建立连接,而这个连接的过程是由内核完成。

在这里插入图片描述
示例:
服务器:


		#include <stdio.h>
		#include <stdlib.h>
		#include <string.h>						
		#include <unistd.h>
		#include <sys/socket.h>
		#include <netinet/in.h>
		#include <arpa/inet.h>				
		int main(int argc, char *argv[])
		{
			unsigned short port = 8000;
			int sockfd;
			sockfd = socket(AF_INET, SOCK_STREAM, 0);// 创建通信端点:套接字
			if(sockfd < 0)
			{
				perror("socket");
				exit(-1);
			}
			struct sockaddr_in my_addr;
			bzero(&my_addr, sizeof(my_addr));	     
			my_addr.sin_family = AF_INET;
			my_addr.sin_port   = htons(port);
			my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
			int err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));
			if( err_log != 0)
			{
				perror("binding");
				close(sockfd);		
				exit(-1);
			}
			err_log = listen(sockfd, 10);
			if(err_log != 0)
			{
				perror("listen");
				close(sockfd);		
				exit(-1);
			}	
			printf("listen client @port=%d...\n",port);
			sleep(10);	// 延时10s
			system("netstat -an | grep 8000");	// 查看连接状态
			return 0;
		}

客户端

		#include <stdio.h>
		#include <unistd.h>
		#include <string.h>
		#include <stdlib.h>
		#include <arpa/inet.h>
		#include <sys/socket.h>
		#include <netinet/in.h>
		int main(int argc, char *argv[])
		{
			unsigned short port = 8000;        		// 服务器的端口号
			char *server_ip = "10.221.20.12";    	// 服务器ip地址
			int sockfd;
			sockfd = socket(AF_INET, SOCK_STREAM, 0);// 创建通信端点:套接字
			if(sockfd < 0)
			{
				perror("socket");
				exit(-1);
			}
			struct sockaddr_in server_addr;
			bzero(&server_addr,sizeof(server_addr)); // 初始化服务器地址
			server_addr.sin_family = AF_INET;
			server_addr.sin_port = htons(port);
			inet_pton(AF_INET, server_ip, &server_addr.sin_addr);
			int err_log = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));      // 主动连接服务器
			if(err_log != 0)
			{
				perror("connect");
				close(sockfd);
				exit(-1);
			}
			system("netstat -an | grep 8000");	// 查看连接状
			while(1);
			return 0;
		}

运行程序时,要先运行服务器,再运行客户端,运行结果如下:
在这里插入图片描述

五、接收请求accept函数(服务器端使用)

accept函数让服务器接收客户端的连接请求。在建立好连接队伍后,服务器就调用accept函数,然后睡眠并等待客户端的连接请求。
accept() 函数会发生阻塞: 从处于established 状态的队列(即已经完成三次握手后)中取出完成的连接,当队列中没有完成连接时候会形成阻塞,直到取出队列中已完成连接的用户连接为止。

(1)头文件:
#include <sys/types.h>
#include <sys/socket.h>

(2)函数作用:用于接收客户端的连接请求。

(3)函数原型:int accpt(int sockfd, const struct sockaddr * client_addr, int client_addrlen);

(4)函数参数:
① sockfd,调用socket()函数返回的套接字描述符
② client_addr,包含远程客户端IP地址和端口号的指针
③ client_addr,包含远程客户端IP地址和端口号的指针的长度,即sizeof(struct sockaddr)

(5)返回值:
① -1:失败,并将错误码保存至errno中,可用perror函数打印。
② 非负数:返回值是一个新的套接字描述符,它表示和客户端的新的连接,可以把它理解成是一个客户端的socket
示例:

		int	acceptedfd;
		unsigned char buf[256];
		acceptedfd = accept(socketfd,(struct sockaddr *)&client_address, (int *)&fromlen);	//接收客户端连接请求
		recv(acceptedfd ,buf,256,0);	//接收客户端发送的数据

六、数据传输(send、recv、close)


  • size_t在不同平台上,其具有不同的定义:
  • /* sparc 64 bit */
  • typedef unsigned long __kernel_size_t;
  • typedef long __kernel_ssize_t;
  • /* sparc 32 bit */
  • typedef unsigned int __kernel_size_t;
  • typedef int __kernel_ssize_t;

1.send()函数

send函数,向一个已经连接的socket发送数据,如果无错误,返回值为所发送数据的总数,否则返回SOCKET_ERROR。
不论是客户还是服务器应用程序都用send函数来向TCP连接的另一端发送数据。客户程序一般用send函数向服务器发送请求,而服务器则通常用send函数来向客户程序发送应答。

(1)头文件:
#include <sys/types.h>
#include <sys/socket.h>

(2)函数作用:向一个已经连接的socket发送数据。

(3)函数原型:ssize_t(int sockfd, const void *buf, size_t len, int flag);

(4)函数参数:
① sockfd,调用socket()函数返回的套接字描述符
② const void *buf,一个指向所要发送的数据的指针
③ len,是以字节为单位的数据的长度
④ flag, 0: 与write()无异,一般情况下填0,特殊情况下才填写下面三种情况
MSG_DONTROUTE:告诉内核,目标主机在本地网络,不用查路由表
MSG_DONTWAIT:将单个I/O操作设置为非阻塞模式
MSG_OOB:指明发送的是带外信息

(5)返回值:
① -1:失败,并将错误码保存至errno中,可用perror函数打印。
② 大于0,表示实际发送的字节数

注意:send函数返回实际上发送出的字节数,可能会少于用户希望发送的数据。在程序中,应该将send函数的返回值与想要发送的字节数(len)进行比较,当send函数的返回值和len长度不一致时候,应该对这种情况进行处理。

2.recv()函数

不论是客户还是服务器应用程序都用recv函数来接收来自TCP连接的另一端所发送过来的数据。

(1)头文件:
#include <sys/types.h>
#include <sys/socket.h>

(2)函数作用:接收TCP另一端发送过来的数据。

(3)函数原型:ssize_t recv(int sockfd, const void *buff, size_t nbytes, int flags);

(4)函数参数:
① sockfd,调用socket()函数返回的套接字描述符
② const void *buf,一个指向接收数据缓冲区的指针
③ len,缓冲区的长度,缓冲区的长度一般定义的较大,防止接收的数据过大,遗漏数据。
④ flag,
0:与read()相同,区别在于read函数读取缓冲区的数据之后,会将缓冲区的数据删除,而recv不会
MSG_DONTWAIT:将单个I/O操作设置为非阻塞模式
MSG_OOB:指明发送的是带外信息
MSG_PEEK:可以查看可读的信息,在接收数据后不会将这些数据丢失
MSG_WAITALL:通知内核直到读到请求的数据字节数时,才返回。

(5)返回值:
① -1:失败,并将错误码保存至errno中,可用perror函数打印。
② 大于0,表示实际接收的字节数

注意:recv接收分为两种情况,一种是客户端,一种是服务器端,示例:

		/*客户端*/
		int rr_1;
		if ( (socketfd = socket(AF_INET, SOCK_STREAM, 0)) == -1 )	//创建套接字
		{	
			perror("Error  to build socket\n");
			return ;
		}
		rr_1 = connect(socketfd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr));		//连接服务器端
		if ( rr_1  == -1 )	
		{	
			perror("Error connecting to socket\n");
			return ;
		}	
		RecvLen = recv(socketfd, RecvBuf,MAXDATASIZE,0);	//接收数据
			
		/*服务器端*/
			socketfd = socket(AF_INET, SOCK_STREAM, 0); 	//创建套接字
			bind(socketfd, (struct sockaddr *)&serv_addr, address_len);	//绑定套接字
			acceptedfd = accept(socketfd,(struct sockaddr *)&client_address, (int *)&fromlen);	//接收客户端的连接请求
			RecvLen=recv(acceptedfd, RecvBuf,MAXDATASIZE,0);	//接收客户端发送过来的数据

3.close()函数

数据操作结束后,就可以调用close()函数来释放该套接字,从而停止在该套接字上的任何数据操作。
(1)头文件:#include <unistd.h>
(2)函数原型:int close(int sockfd);
(3)函数参数: sockfd,所要释放的套接字描述符

4.shutdown()函数

该函数也是关闭套接字的,但是和close不同的是,该函数允许只停止在某个方向上的数据传输,而另一个方向上的数据传输继续进行。

(1)头文件:#include <sys/socket.h>

(2)函数原型:int shutdown(int sockfd,int how)

(3)函数参数:
① sockfd,所要关闭的套接字描述符
② how,允许为关闭操作选择以下几种方式:
0:不允许继续接收数据
1:不允许继续发送数据
2:不允许继续发送和接收数据
如果以上几种行为都不允许,可以直接调用close函数。

(4)返回值:0:成功,-1:失败,并设置相应errno值

七、TCP通信流程

1.服务器端

  1. 创建套接字socket()
  2. 配置服务器sockaddr_in结构体
  3. 绑定套接字bind()
  4. 监听模式,通知Linux内核三次握手listen()
  5. 等待客户端连接,调用client_fd = accep()
  6. 接收客户端信息,client_fd=accept(sockfd,NULL,NULL)
  7. 发送消息给客户端,(send(client_fd,sendbuf,strlen(sendbuf),0)

2.客户端

  1. 创建套接字socket()
  2. 填写所要连接的服务器sockaddr_in结构体
  3. 主动发起对服务端的链接,调用connect()
  4. 发送消息给服务器,send(sockfd,buf,strlen(buf),0)
  5. 接收服务器的消息,recv(sockfd,buf,BUFFER_SIZE,0)

八、TCP通信示例

1.服务器端

		///server.c
		#include <sys/types.h>
		#include <sys/socket.h>
		#include <stdio.h>
		#include <stdlib.h>
		#include <errno.h>
		#include <string.h>
		#include <unistd.h>
		#include <netinet/in.h>
		#define PORT 4321
		#define BUFFER_SIZE 1024
		#define MAX 5
		#include <pthread.h>
		int main()
		{
		    struct sockaddr_in servaddr;
		    int sockfd,client_fd;
		    char buf[BUFFER_SIZE];
			char sendbuf[] = "received data";
			/*建立socket连接*/
		    if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
		    {
		        perror("socket");
		        exit(1);
		    }
		
		    printf("socket id=%d\n",sockfd);
			/*设置sockaddr_in结构体中相关参数*/
		    bzero(&servaddr,sizeof(servaddr));
		    servaddr.sin_family=AF_INET;
		    servaddr.sin_port=htons(PORT);
		    servaddr.sin_addr.s_addr=INADDR_ANY;
		    int i=1;  /*允许重复使用本地址与套接字进行绑定*/
		    setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&i,sizeof(i));
			
			/*绑定函数bind()*/
		    if(bind(sockfd,(struct sockaddr *) &servaddr,sizeof(servaddr))==-1)
		    {
		        perror("bind");
		        exit(1);
		    }
		    printf("Bind success!\n");
			/*调用listen函数,创建未处理请求的队列*/
		    if(listen(sockfd,MAX)==-1)
		    {
		        perror("listen");
		        exit(1);
		    }
		
		    printf("Listen...\n");
		
			/*调用accept函数,等待客户端连接*/
		    if((client_fd=accept(sockfd,NULL,NULL))==-1)
		    {
		        perror("accept");
		        exit(0);
		    }
		    
			while(1)
			{
				if(recv(client_fd,buf,BUFFER_SIZE,0)==-1)
				{
				    perror("recv");
				    exit(0);
				}
				else
				{	
					printf("Received a message:%s\n",buf);
					memset(buf,0,sizeof(buf));
					if(send(client_fd,sendbuf,strlen(sendbuf),0)==-1)
					{
						perror("send");
						exit(-1);
					}
					memset(buf,0,sizeof(buf));
				}
			}
		    close(sockfd);
		    exit(0);
		}

2.客户端

		///client.c
		#include <stdio.h>
		#include <stdlib.h>
		#include <errno.h>
		#include <string.h>
		#include <netdb.h>
		#include <sys/types.h>
		#include <sys/socket.h>
		#include<arpa/inet.h>
		#define BUFFER_SIZE 100
		
		int main(int argc,char *argv[])
		{
		    int sockfd,client_fd;
			int len;
		    char buf[BUFFER_SIZE];
		    struct sockaddr_in servaddr;
		
		    if(argc<3)
		    {
		        printf("USAGE=%s <serv_in> <serv_port>\n",argv[0]);
		        exit(-1);
		    }

			/*创建socket*/
		    if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
		    {
		        perror("socket");
		        exit(-1);
		    }
			else
			{
				printf("socket build success\r\n");
			}
		  
			/*创建sockaddr_in结构体中相关参数*/
		    bzero(&servaddr,sizeof(servaddr));
		    servaddr.sin_family=AF_INET;
		    servaddr.sin_port=htons(atoi(argv[2]));
		    servaddr.sin_addr.s_addr= inet_addr(argv[1]);
			
			/*调用connect函数主动发起对服务端的链接*/
		    if(connect(sockfd,(struct sockaddr *) &servaddr,sizeof(servaddr))==-1)
		    {
		        perror("connect");
		        exit(-1);
		    }
		    
			/*发送消息给服务端*/
			while(1)
			{
				printf("please input string:\r\n");					
				scanf("%s",buf);
				len = sizeof(buf);
				printf("len = %d\r\n",len);
				buf[len] = 0;
				if(send(sockfd,buf,strlen(buf),0)==-1)
				{
				    perror("send");
				    exit(-1);
				}
				memset(buf,0,sizeof(buf));
				if(recv(sockfd,buf,BUFFER_SIZE,0)==-1)
				{
				    perror("recv");
				    exit(0);
				}
				printf("Received a message:%s\n",buf);
				memset(buf,0,sizeof(buf));
			}
		    close(sockfd);
		    exit(0);
		}

运行结果:
先运行服务器端,再运行客户端,客户端输入服务器的IP地址(注意,我这里填的10.144.42.88是我虚拟机的IP地址,查看自己IP地址可以使用ifcong命令)和端口号,回车之后,输入想要发送给服务器的字符串,点回车,发送成功之后,服务器端会回复receive data的信息,表示接收成功。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_37733540/article/details/95046565