socket编程之实现一个简单的TCP通信

学习socket编程,实现一个简单的TCP通信。

  • 基础知识学习:网络通信、tcp/ip 协议、服务器端客户端、socket;
  • socket函数详解;
  • TCP协议通讯的实现举例。

1. 基础知识学习

  1. 网络中进程之间如何通信?

    本地的进程间通信(IPC)可以总结为下面4类:
    1.消息传递(管道、FIFO、消息队列)
    2.同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
    3.共享内存(匿名的和具名的)
    4.远程过程调用(Solaris门和Sun RPC)

    但这些都不是本文的主题!我们要讨论的是网络中进程之间如何通信?
    首要问题是如何唯一标识一个进程,否则通信无从谈起!
    在本地可以通过进程PID来唯一标识一个进程,但在网络中是依靠TCP/IP协议族。网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

    使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)来实现网络进程之间的通信。所以说“socket无处不在”。
    在这里插入图片描述
    在这里插入图片描述

  2. 理解TCP/IP协议:
    TCP/IP协议叫做传输控制 / 网络协议,又叫网络通信协议。
    TCP负责发现传输的问题,一旦有问题就会发出重传信号,直到所有数据安全正确的传输到目的地。

  3. 理解服务器端与客户端:
    1.服务器(server):
    是指网络中能对其它机器提供某些服务的计算机系统(如果一个PC对服务器端外提供ftp服务,也可以叫服务器)。
    2.客户端(client):
    或称用户端,是指与服务器相对应,为客户提供本地服务的程序。
    3.区别:
    服务器端是为客户端服务的,客户端是为真正的“客户”来服务的;
    客户端是请求方或者说是指令发出方,而服务器端是响应方;
    (1)客户端:在web中是以request对象存在的,发送请求给服务器端处理;
    (2)服务端:顾名思义是服务的,客户端发送的请求交给服务器端处理,是以response对象存在,服务器端处理完毕后反馈给客户端。
    (3)一般我们访问网站,都是客户端(浏览器、app)发出请求,然后对方服务器端(sina,sohu)响应,结果就是返回了页面路径给我们,我们再根据路径看到了网页。

  4. 什么是socket?
    socket起源于Unix,就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),这些函数我们在后面进行介绍。

    socket即为套接字,在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一的标识网络通讯中的一个进程,“IP地址+TCP或UDP端口号”就为socket。

    在TCP协议中,建立连接的两个进程(如客户端和服务器)各自有一个socket来标识,则这两个socket组成的socket pair就唯一标识一个连接。

    socket本身就有“插座”的意思,因此用来形容网络连接的一对一关系,为TCP/IP协议设计的应用层编程接口称为socket API。

2.socket的基本操作,函数

既然socket是“open—write/read—close”模式的一种实现,那么socket就提供了这些操作对应的函数接口。下面以TCP为例,介绍几个基本的socket接口函数。

头文件:<sys/types.h> ,<sys/socket.h>

  1. 创建一个socket描述符:
    int socket(int domain,int type,int protocol);
    在这里插入图片描述

    ① domain: <协议族> 一般被设置为AF_INET,表示使用的是IPv4地址。
    协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合。
    ② type: <socket类型> 我们这里实现的是TCP,因此选用SOCK_STREAM 面向流,如果实现UDP可选SOCK_DGRAM 数据报。
    ③ protocol <协议类型> 一般使用默认,设置为0。
    并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。

    成功返回一个socket(文件描述符),出错则返回-1。

    该函数用于打开一个网络通讯接口,应用进程就可以像读写文件一样调用read/write在网络上收发数据。

    当我们调用socket创建一个socket时,返回的socket描述字存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则当调用connect()、listen()时系统就会自动随机分配一个端口。

  2. 套接字绑定本地的地址和端口:
    int bind(int sockfd,const struct sockaddr*addr,socklen_t addrlen);
    在这里插入图片描述

    ① sockfd: 即socket描述字,它是通过socket()函数创建,唯一标识一个socket。
    ② addr: 一个struct sockaddr *指针,指向要绑定给sockfd的协议地址。
    这个地址结构根据地址创建socket时的地址协议族的不同而不同。ipv4对应的地址结构体下面会讲到。
    ③ addrlen: 地址的长度。

    成功返回0,出错返回-1。
    bind()函数把一个地址族中的特定地址赋给socket,即将参数sockfd和addr绑定在一起。例如对应AF_INET就是把一个ipv4地址和端口号组合赋给socket。

    通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

    其中ipv4对应的地址结构体是怎样定义的呢? 详情点这里

    socket绑定的ip为INADDR_ANY 的意义是什么?点这里

  3. 监听 与 请求连接:
    如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。

    int listen(int sockfd,int backlog);
    在这里插入图片描述

    ① sockfd:  要监听的socket描述字
    ② backlog : 内核为次套接口排队的最大数量,这个大小一般为5~10,不宜太大(是为了防止SYN攻击)

    listen()成功返回0,失败返回-1。
    该函数仅被服务器端使用,listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果收到更多的连接请求就忽略。
    socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

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

    ① sockfd : 客户端的socket描述字
    ② addr :   服务器的socket地址
    ③ addrlen : socket地址的长度。注意他的类型为socklen_t *,不要定义为int

    在这里插入图片描述connect()成功返回0,出错返回-1。
    这个函数只需要有客户端程序来调用,调用该函数后表明连接服务器,客户端通过调用connect函数来建立与TCP服务器的连接。

  4. 接收连接
    int accept(int sockfd,struct sockaddr* addr,socklen_t* addrlen);
    在这里插入图片描述

    ① sockfd :服务器的socket描述字
    ② addr : 指向struct sockaddr *的指针,用于返回客户端的协议地址
    ③ addrlen :协议地址的长度。 注意类型为socklen_t,不要定义成int,socklen_t 在头文件#include <sys/socket.h>中有定义。https://blog.csdn.net/blueliuyun/article/details/7653583

    成功返回由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接;失败返回-1。

    TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

    accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

    典型的服务器程序是可以同时服务多个客户端的,当有客户端发起连接时,服务器就调用accept()返回并接收这个连接,如果有大量客户端发起请求,服务器来不及处理,还没有accept的客户端就处于连接等待状态。

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

  5. 关闭套接字
    int close(int fd);
    头文件 :#include <unistd.h>

    在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

    close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。

    注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。

3.TCP协议通讯的实现举例

  1. 服务器编程思路:

    (1)调用socket() 创建一个套接字用来通讯(两个套接字:1本身服务器需要一个套接字,2客户端返回一个套接字)
    地址处理以及端口处理
    (2)调用bind() 绑定这个套接字与本地的地址和端口
    (3)调用listen() 来监听端口是否有客户端请求来
    (4)如果有,就调用accept()进行连接,返回一个用连接的新的套接字。否则就继续阻塞式等待直到有客户端连接上来。
    (5)新套接字通信
    (6)关闭套接字

  2. 客户端编程思路:
    (1)调用socket()分配一个用来通讯的端口
    (2)调用connect()发出SYN请求并处于阻塞等待服务器应答状态,服务器应答一个SYN-ACK分段,客户端收到后从connect()返回,同时应答一个ACK分段,服务器收到后从accept()返回,连接建立成功。客户端一般不调用bind()来绑定一个端口号,并不是不允许bind(),服务器也不是必须要bind()。

注意服务器客户两端的结构体一定要事先商量好一致。

server.c:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
 
#define PORT 10001	
#define BACKLOG 10	
 
struct data{
	int a;
	int b;
	int c;
	int sentence_len;
};

int main()
{
	int iSocketFD,new_fd;//创建2个套接字,1作为本机套接字,2接收客户
	int iRecvLen;
	char buf[1000];
	struct sockaddr_in server_addr,client_addr;//1接受本地ip,2用来接收客户端的socket地址结构体
 
	iSocketFD = socket(AF_INET, SOCK_STREAM, 0); 
	if(0 > iSocketFD)
	{
		perror("socket");  
		return -1;
	}	
	//printf("iSocketFD: %d\n", iSocketFD);	
        
	//地址处理及端口处理
	bzero(&server_addr,sizeof(server_addr));
	server_addr.sin_family = AF_INET;//初始化地址家族
	server_addr.sin_port = htons(PORT); //设置本地端口
	server_addr.sin_addr.s_addr=htons(INADDR_ANY);//INADDR_ANY表示任何ip地址都可以接入,htons可以省略
        
    //设置套接字选项避免地址使用错误 
    int on=1;  
    if((setsockopt(iSocketFD,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)))<0)  
    {  
		perror("setsockopt");  
		return -1;
    } 

	//绑定
	if(0 > bind(iSocketFD,(struct sockaddr *)&server_addr, sizeof(server_addr)))//bind(套接字,地址 ,长度)
	{
		perror("connect");
		return -1;
	}

	//监听
	if(0 > listen(iSocketFD, BACKLOG))//BACKLOG代表监听个数或返回标记
	{
		perror("listen");
		return -1;
	}

	socklen_t socklen = sizeof(client_addr);

	//收到消息,返回一个新的文件描述符
	new_fd = accept(iSocketFD, (void *)&client_addr, &socklen);
	if(0 > new_fd)
	{
		perror("accept");
		return -1;
	}
	else
	{
		printf("get a client, ip:%s, port:%d\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
		char *buff = (char *)malloc(sizeof(struct data));
		struct data *pdata = (struct data *)buff;
		int recv1 = recv(new_fd,buff,sizeof(struct data),0);	
		buff = realloc(buff,sizeof(struct data) + ntohl(pdata->sentence_len));
		int recv2 = recv(new_fd,buff+sizeof(struct data),ntohl(pdata->sentence_len),0);

		pdata->a = ntohl(pdata->a);
		pdata->b = ntohl(pdata->b);
		pdata->c = ntohl(pdata->c);
		pdata->sentence_len = ntohl(pdata->sentence_len);
		printf("recv:a[%d] b[%d] c[%d][%s]\n",pdata->a,pdata->b,pdata->c,buff+sizeof(struct data));
		printf("recv1_num[%d] recv2_num[%d]\n",recv1,recv2);

	}

	close(new_fd);
	close(iSocketFD);

	return 0;
}

client.c:

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <errno.h>

#define SERVER_PORT 10001
#define SERVER_ADDR "192.168.18.133"

struct data{
	int a;
	int b;
	int c;
	int sentence_len;
};


int main() 
{
	int iSocketFD;
	socklen_t socklen;
	char buf[1000];
	struct sockaddr_in serveraddr;

	iSocketFD = socket(AF_INET, SOCK_STREAM, 0);
	if(0 > iSocketFD)
	{
		perror("socket");  
		return -1; 
	}
	//printf("iSocketFD: %d\n", iSocketFD);

	//需要connect的是对端的地址,因此这里定义服务器端的地址结构体
	bzero(&serveraddr,sizeof(serveraddr)); 
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(SERVER_PORT);
	serveraddr.sin_addr.s_addr=inet_addr(SERVER_ADDR);
	//inet_aton(SERVER_ADDR, &serveraddr.sin_addr);

	//连接请求
	if(0 > connect(iSocketFD, (struct sockaddr *)&serveraddr, sizeof(serveraddr)))
	{
		printf("connect failed:%d,%s",errno,strerror(errno));
		return -1;
	}
	else
	{
		perror("accept");  
		//struct data *data1 =  create_send_log(data1);
		
		struct data *data1 = (struct data *)malloc(sizeof(struct data));
		data1->a = 3;
		data1->b = 5;
		data1->c = 9;
		data1->sentence_len = sizeof("this is a sentence!");

		data1->a = htonl(data1->a); 
		data1->b = htonl(data1->b);
		data1->c = htonl(data1->c);
		data1->sentence_len = htonl(data1->sentence_len);
		int send1 = send(iSocketFD, data1, sizeof(struct data), 0); 
		int send2 = send(iSocketFD, "this is a sentence!", ntohl(data1->sentence_len), 0); 
		printf("send:a[%d] b[%d] c[%d] len[%d]\n",ntohl(data1->a),ntohl(data1->b),ntohl(data1->c),ntohl(data1->sentence_len));
		printf("send1_num[%d] send2_num[%d]\n",send1,send2);
	}

	close(iSocketFD);
	return 0;
}

4.参考文章如下:

https://blog.csdn.net/qq_33951180/article/details/68066634
https://blog.csdn.net/jinmie0193/article/details/78951055#commentBox
https://blog.csdn.net/u011270542/article/details/80077883

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值