学习socket编程,实现一个简单的TCP通信。
- 基础知识学习:网络通信、tcp/ip 协议、服务器端客户端、socket;
- socket函数详解;
- TCP协议通讯的实现举例。
1. 基础知识学习
-
网络中进程之间如何通信?
本地的进程间通信(IPC)可以总结为下面4类:
1.消息传递(管道、FIFO、消息队列)
2.同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
3.共享内存(匿名的和具名的)
4.远程过程调用(Solaris门和Sun RPC)但这些都不是本文的主题!我们要讨论的是网络中进程之间如何通信?
首要问题是如何唯一标识一个进程,否则通信无从谈起!
在本地可以通过进程PID来唯一标识一个进程,但在网络中是依靠TCP/IP协议族。网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)来实现网络进程之间的通信。所以说“socket无处不在”。
-
理解TCP/IP协议:
TCP/IP协议叫做传输控制 / 网络协议,又叫网络通信协议。
TCP负责发现传输的问题,一旦有问题就会发出重传信号,直到所有数据安全正确的传输到目的地。 -
理解服务器端与客户端:
1.服务器(server):
是指网络中能对其它机器提供某些服务的计算机系统(如果一个PC对服务器端外提供ftp服务,也可以叫服务器)。
2.客户端(client):
或称用户端,是指与服务器相对应,为客户提供本地服务的程序。
3.区别:
服务器端是为客户端服务的,客户端是为真正的“客户”来服务的;
客户端是请求方或者说是指令发出方,而服务器端是响应方;
(1)客户端:在web中是以request对象存在的,发送请求给服务器端处理;
(2)服务端:顾名思义是服务的,客户端发送的请求交给服务器端处理,是以response对象存在,服务器端处理完毕后反馈给客户端。
(3)一般我们访问网站,都是客户端(浏览器、app)发出请求,然后对方服务器端(sina,sohu)响应,结果就是返回了页面路径给我们,我们再根据路径看到了网页。 -
什么是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>
-
创建一个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()时系统就会自动随机分配一个端口。
-
套接字绑定本地的地址和端口:
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 的意义是什么?点这里
-
监听 与 请求连接:
如果作为一个服务器,在调用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 *,不要定义为intconnect()成功返回0,出错返回-1。
这个函数只需要有客户端程序来调用,调用该函数后表明连接服务器,客户端通过调用connect函数来建立与TCP服务器的连接。 -
接收连接
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()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。
-
关闭套接字
int close(int fd);
头文件 :#include <unistd.h>在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。
close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
3.TCP协议通讯的实现举例
-
服务器编程思路:
(1)调用socket() 创建一个套接字用来通讯(两个套接字:1本身服务器需要一个套接字,2客户端返回一个套接字)
地址处理以及端口处理
(2)调用bind() 绑定这个套接字与本地的地址和端口
(3)调用listen() 来监听端口是否有客户端请求来
(4)如果有,就调用accept()进行连接,返回一个用连接的新的套接字。否则就继续阻塞式等待直到有客户端连接上来。
(5)新套接字通信
(6)关闭套接字 -
客户端编程思路:
(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