【Unix 网络编程】TCP 客户/服务器简单 Socket 程序

前面介绍了Socket 编程函数  在编写 Socket 程序之前还需要了解TCP连接是如何建立的。(参考资料《Unix 网络编程》)

建立一个 TCP 连接时会发生下述情形:

1. 服务器必须准备好接受外来的连接。这通常通过调用 socket、bind 和 listen 这三个函数来完成,我们称之为被动打开。

2. 客户通过调用 connect 发起主动打开,这导致客户TCP发送一个SYN(同步)分节,标识希望连接的服务器端口以及初始序号。通常SYN分节不携带数据,其所在IP数据报只含有一个IP首部、一个TCP首部及可能有的TCP选项。

3. 服务器发送回一个包含服务器初始序号以及对客户端 SYN 段确认的 SYN + ACK 段作为应答,由于一个 SYN 占用一个序号,因此确认序号设置为客户端初始序号加 1。

4. 客户端发送确认序号为服务器初始序号加 1 的 ACK 段,对服务器 SYN 段进行确认。

这种交换至少需要三个分组,因此称之为TCP的三路握手。

一旦TCP建立连接,客户/服务器之间便可以进行数据通信。


1. 服务器端

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<netinet/in.h>
#include <errno.h>  
#define PORT 6666

int main(int argc,char **argv)
{
	int ser_sockfd,cli_sockfd;
	int err,n;
	int addlen;
	struct sockaddr_in ser_addr;
	struct sockaddr_in cli_addr;
	char recvline[200],sendline[200];
	
	ser_sockfd = socket(AF_INET,SOCK_STREAM,0);          //创建套接字
	if(ser_sockfd == -1)
	{
		printf("socket error:%s\n",strerror(errno));
		return -1;
	}
	
	bzero(&ser_addr,sizeof(ser_addr));
	
	/*在待捆绑到该TCP套接口(sockfd)的网际套接口地址结构中填入通配地址(INADDR_ANY)
	和服务器的众所周知端口(PORT,这里为6666),这里捆绑通配地址是在告知系统:要是系统是
	多宿主机(具有多个网络连接的主机),我们将接受宿地址为任何本地接口的地址*/     
	ser_addr.sin_family = AF_INET;
	ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	ser_addr.sin_port = htons(PORT);
	
	//将网际套接口地址结构捆绑到该套接口
	err = bind(ser_sockfd,(struct sockaddr *)&ser_addr,sizeof(ser_addr));  
	if(err == -1)
	{
		printf("bind error:%s\n",strerror(errno));
		return -1;
	}
	//将套接口转换为一个监听套接口,监听等待来自客户端的连接请求
	err = listen(ser_sockfd,5);                                      
	if(err == -1)
	{
		printf("listen error\n");
		return -1;
	}
	
	printf("listen the port:\n");
	
	while(1)
	{	
		addlen = sizeof(struct sockaddr);
		//等待阻塞,等待客户端申请,并接受客户端的连接请求
		//accept成功,将创建一个新的套接字,并为这个新的套接字分配一个套接字描述符
		cli_sockfd = accept(ser_sockfd,(struct sockaddr *)&cli_addr,&addlen);   
		if(cli_sockfd == -1)
		{
			printf("accept error\n");
		}
		
		//数据传输
		while(1)
		{
			printf("waiting for client...\n");
			n = recv(cli_sockfd,recvline,1024,0);
			if(n == -1)
			{
				printf("recv error\n");
			}
			recvline[n] = '\0';
			
			printf("recv data is:%s\n",recvline);
			
			printf("Input your words:");
			scanf("%s",sendline);
			send(cli_sockfd,sendline,strlen(sendline),0);
		}
		close(cli_sockfd);
	}
	close(ser_sockfd);
	
	return 0;
}

1.首先通过 socket 函数创建套接字,此时套接字数据结构字段并未填充,在使用之前必须调用过程来填充对应字段,这里在地址结构中填入通配地址(INADDR_ANY),通配地址就是指定地址为 0.0.0.0 的地址,表示服务器接受机器上所有IP地址的连接,用于多IP机器上。这样无论客户 connect 哪个IP地址,服务器端都会接收到请求,即接受宿地址为任何本地接口的地址。如果是指定地址,那么机器只有 connect 这个地址才能成功。后面是填充端口号,如果指定为 0,则由系统随机选择一个未被使用的端口。

2. bind 将没有指定端口的 socket(ser_sockfd)绑定到我们指定的端口上(通配地址+指定端口号),服务器是通过它们的众所周知端口被大家认识的。这样 socket(ser_sockfd)就与指定的端口产生了关联,即指向了指定端口。

3. listen 将套接口转换为一个监听套接口,被动打开,允许监听客户端的连接请求,然后 accept 客户端的连接请求,没有请求则阻塞。

5. accept 成功后,将创建新的套接字,并为新套接字分配一个套接口描述符,该套接字除了记录本地(服务器)的IP和端口号信息外,还记录了目的(客户)IP和端口号信息。服务器与客户的通信则是通过该新创建的套接字(已连接套接字)进行。


2. 客户端

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<netinet/in.h>
#define PORT 6666

int main(int argc,char **argv)
{
	int sockfd;
	int err,n;
	struct sockaddr_in addr_ser;
	char sendline[200],recvline[200];
	
	sockfd = socket(AF_INET,SOCK_STREAM,0);       //创建套接字
	if(sockfd == -1)
	{
		printf("socket error\n");
		return -1;
	}
	
	bzero(&addr_ser,sizeof(addr_ser));     
	
	/*用通配地址和指定端口号装填一个网际接口地址结构*/ 
	addr_ser.sin_family = AF_INET;
	addr_ser.sin_addr.s_addr = htonl(INADDR_ANY);   
	addr_ser.sin_port = htons(PORT);                
	
	//TCP:客户(sockfd)向服务器(套接口地址结构)发起连接,主动请求
	//服务器的IP地址和端口号有参数addr_ser指定
	err = connect(sockfd,(struct sockaddr *)&addr_ser,sizeof(addr_ser));   
	if(err == -1)
	{
		printf("connect error\n");
		return -1;
	}
	
	printf("connect with server...\n");
	
	//数据传输
	while(1)
	{ 	
		printf("Input your words:");
		scanf("%s",sendline);
		
		send(sockfd,sendline,strlen(sendline),0);            
	
		printf("waiting for server...\n");
	
		n = recv(sockfd,recvline,100,0);                      
		recvline[n] = '\0'; 
		
		printf("recv data is:%s\n",recvline);
	}
	
	return 0;
}
1. 客户端同样通过 socket 创建套接字,TCP 客户通常不把IP地址捆绑到它的套接口上,当连接套接口时,内核将根据所用外出网络接口来确定源IP地址,并选择一个临时端口作为源端口。
2. 用通配地址和指定端口填充的是待连接服务器端的套接字地址结购,这里采用的是通配地址,由于服务器端指定的是通配地址,即接受机器上所有IP地址的连接,同样客户也可向机器上任何IP地址发起连接,服务器端都会接收到。

3. 客户端向服务器发起连接请求,connect 成功后,其请求连接的服务器的IP和端口号信息将会写入该套接字,这样该套接字也同时记录了本地和目的的IP地址和端口信息。也就可以进行通信了。

结果如下:

总的说来:

对于服务器程序而言,编程步骤如下:

调用socket,创建一个不与任何网络连接相对应的套接口(socket),其返回值是一个文件描述符;调用bind,将刚刚创建的 socket 与本机的 IP地址和端口号绑定,这样一来 socket 就变成了一个 3元组;调用 listen,将 socket 指定为一个被动套接口(又称监听套接口),专门用于监听客户端到达的网络连接请求;调用 accept,使 socket 进入到可以接收网络连接请求的状态,此时服务器将能够接收客户端发起的连接,但如果没有客户端发起连接,那么服务器将进入阻塞态,直到有客户端连接到达,accept 将返回;此时,accept 的返回值是另一个 socket(文件描述符),这个 socket 被称为连接套接口,此套接口是一个5元组(其中远端IP和远端端口号来自于客户机发起连接时发送到服务器的数据包,其实就是 3次握手的第一个数据包);调用 read,从连接套接口接收客户端发送过来的数据,如果此时客户端并未发送数据,那么 read将阻塞到有数据到达为止;根据客户端的要求进行数据处理,调用 write,向连接套接口写入处理后的数据,该数据将通过网络达到客户端;当所有数据都已处理完毕,将调用 close 关闭连接套接口,这将在物理上激发 4分节终止序列,在客户机的配合下,成功完成网络连接的关闭,自此通信结束。

对于客户机程序而言,编程步骤如下:

调用 socket,创建一个不与任何网络连接相对应的套接口(socket);调用 connect,将服务器 IP和端口号作为参数输入,系统将选择一个本机尚未使用的大于 1024的端口号作为本地端口号(此时,5元组已经齐备),主动发起连接(这将激发3次握手),当3次握手成功完成后,connect 将返回,此时 socket 已经物理上对应了一个网络连接通道;调用 write 向 socket 写入数据,该数据将达到服务器,被 read所读取,然后调用 read,等待从 socket 获取服务器通过 write 回送的数据;当所有数据都接收完毕,调用 close,激发或者配合完成 4分节终止序列,成功完成网络连接的关闭,自此通信结束。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux网络编程TCP/IP基础篇 01TCPIP基础(一) ISO/OSI参考模型 TCP/IP四层模型 基本概念(对等通信、封装、分用、端口) 02TCPIP基础(二) 最大传输单元(MTU)/路径MTU 以太网帧格式 ICMP ARP RARP 03TCPIP基础(三) IP数据报格式 网际校验和 路由 04TCPIP基础(四) TCP特点 TCP报文格式 连接建立三次握手 连接终止四次握手 TCP如何保证可靠性 05TCPIP基础(五) 滑动窗口协议 UDP特点 UDP报文格式 Linux网络编程socket编程篇 06socket编程(一) 什么是socket IPv4套接口地址结构 网络字节序 字节序转换函数 地址转换函数 套接字类型 07socket编程(二) TCP客户/服务器模型 回射客户/服务器 socket、bind、listen、accept、connect 08socket编程(三) SO_REUSEADDR 处理多客户连接(process-per-conection) 点对点聊天程序实现 09socket编程(四) 流协议与粘包 粘包产生的原因 粘包处理方案 readn writen 回射客户/服务器 10socket编程(五) read、write与recv、send readline实现 用readline实现回射客户/服务器 getsockname、getpeername gethostname、gethostbyname、gethostbyaddr 11socket编程(六) TCP回射客户/服务器 TCP是个流协议 僵进程与SIGCHLD信号 12socket编程(七) TCP 11种状态 连接建立三次握手、连接终止四次握手 TIME_WAIT与SO_REUSEADDR SIGPIPE 13socket编程(八) 五种I/O模型 select 用select改进回射客户端程序 14socket编程(九) select 读、写、异常事件发生条件 用select改进回射服务器程序。 15socket编程(十) 用select改进第八章点对点聊天程序 16socket编程(十一) 套接字I/O超时设置方法 用select实现超时 read_timeout函数封装 write_timeout函数封装 accept_timeout函数封装 connect_timeout函数封装 17socket编程(十二) select限制 poll 18socket编程(十三) epoll使用 epoll与select、poll区别 epoll LT/ET模式 19socket编程(十四) UDP特点 UDP客户/服务基本模型 UDP回射客户/服务器 UDP注意点 20socket编程(十五) udp聊天室实现 21socket编程(十六) UNIX域协议特点 UNIX域地址结构 UNIX域字节流回射客户/服务 UNIX域套接字编程注意点 22socket编程(十七) socketpair sendmsg/recvmsg UNIX域套接字传递描述符字 Linux网络编程之进程间通信篇 23进程间通信介绍(一) 进程同步与进程互斥 进程间通信目的 进程间通信发展 进程间通信分类 进程间共享信息的三种方式 IPC对象的持续性 24进程间通信介绍(二) 死锁 信号量 PV原语 用PV原语解决司机与售票员问题 用PV原语解决民航售票问题 用PV原语解决汽车租赁问题 25System V消息队列(一) 消息队列 IPC对象数据结构 消息队列结构 消息队列在内核中的表示 消息队列函数 26System V消息队列(二) msgsnd函数 msgrcv函数 27System V消息队列(三) 消息队列实现回射客户/服务器 28共享内存介绍 共享内存 共享内存示意图 管道、消息队列与共享内存传递数据对比 mmap函数 munmap函数 msync函数 29System V共享内存 共享内存数据结构 共享内存函数 共享内存示例 30System V信号量(一) 信号量 信号量集结构 信号量集函数 信号量示例 31System V信号量(二) 用信号量实现进程互斥示例 32System V信号量(三) 用信号集解决哲学家就餐问题 33System V共享内存与信号量综合 用信号量解决生产者消费者问题 实现shmfifo 34POSIX消息队列 POSIX消息队列相关函数 POSIX消息队列示例 35POSIX共享内存 POSIX共享内存相关函数 POSIX共享内存示例 Linux网络编程之线程篇 36线程介绍 什么是线程 进程与线程 线程优缺点 线程模型 N:1用户线程模型 1:1核心线程模型 N:M混合线程模型 37POSIX线程(一) POSIX线程库相关函数 用线程实现回射客户/服务器 38POSIX线程(二) 线程属性 线程特定数据 39POSIX信号量与互斥锁 POSIX信号量相关函数 POSIX互斥锁相关函数 生产者消费者问题 自旋锁与读写锁介绍 40POSIX条件变量 条件变量 条件变量函数 条件变量使用规范 使用条件变量解决生产者消费者问题 41一个简单的线程池实现 线程池性能分析 线程池实现

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值