网络编程就不得不提大名鼎鼎的套接字—Socket
一,什么是Socket
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个Socket。Socket的英文原意是“插座”,通常称之为套接字,来描述IP地址和端口,是一个通信链的句柄,用来实现不同虚拟机或者计算机之间的通信。
在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,与不同客户端的不同服务对应着不同的Socket,这样实现了与多个服务器进行不同服务的功能,因为Socket的不同,这些客户端或服务不会混淆。
应用层通过传输层进行数据通信时,TCP和UDP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了称为套接字 (Socket)的接口,区分不同应用程序进程间的网络通信和连接。
二,套接字的类型
1. 流式套接字(SOCK_STREAM)
提供面向连接,可靠的数据传输服务,数据无差错,无重复的发送,且按发送顺序接受。
2. 数据报式套接字(SOCK_DGRAM)
提供了一个无连接服务。数据包以独立包形式被发送,不提供无错保证,数据可能丢失或重复,并且接受顺序混乱。网络文件系统(NFS)使用数据报式套接字。
3. 原始套接字(SOCK_RAW)
该接口允许对较低层协议,如IP,ICMP直接访问。常用于检验新的协议实现或者访问现在服务中配置的新设备。
三,基于TCP(有连接)/UDP(无连接)的Socket编程
要通过互联网进行通信,至少需要一对套接字,一个运行于客户机端,称之为ClientSocket,另一个运行于服务器端,称之为serverSocket。
根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可分为以下几个步骤:
tcp中服务器端的流程如下:
1. 创建套接字(Socket)。
2. 将套接字绑定到一个本地地址和端口上(bind)。
3. 将套接字设为监听模式,准备接受客户请求(listen)。
4. 等待客户请求到来,当请求到来后,接受连接请求。返回一个新的对应于此次连接的套接字(accept)。
5. 用于返回的套接字和客户端进行通信(send/recv)。
6. 返回,等待另一客户请求。
7. 关闭套接字(close)。
tcp中客户端流程如下:
1. 创建套接字(socket)。
2. 向服务器发出连接请求(connect)。
3. 和服务器端进行通信(recv/send)。
4. 关闭套接字(close)。
无连接的udp服务器端流程如下:
1. 创建套接字(socket)。
2. 将套接字绑定到一个本地地址和端口上(bind)。
3. 等待接收数据(recvfrom)。
4. 关闭套接字(close)。
udp客户端流程:
1. 创建套接字(socket)。
2. 向服务器发送数据(sendto)。
3. 关闭套接字(close)。
四,相关函数说明
要运用套接字进行网络通信,有关套接字的几个重要函数是必须要掌握的,在上述TCP/UDP网络通信中出现的函数有 socket , bind , listen , accept和connect , send和recv , close。
创建套接字—socket()
应用程序在使用套接字前,首先必须拥有一个套接字,系统调用socket()向应用程序提供创建套接字的手段,其调用格式如下:
#include <sys/socket.h>
int socket(int family, int type, int protocol);
该调用要接收三个参数: family、type、protocol。
参数 family指定通信发生的区域,UNIX系统支持的地址族有:AF_UNIX、AF_INET、AF_NS等,而DOS、WINDOWS中仅支持AF_INET,它是网际网区域。因此,地址族与协议族相同。
参数type 描述要建立的套接字的类型。
参数protocol说明该套接字使用的特定协议,如果调用者不希望特别指定使用的协议,则置为0,使用默认的连接模式。
根据这三个参数建立一个套接字,并将相应的资源分配给它,同时返回一个整型套接字号。
绑定套接字—bind()
bind函数把一个本地协议地址赋予一个套接字,对于网际网协议,协议地址是32位的IPv4地址或者是128位的IPv6地址与16位的TCP或UDP端口号的组合。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
第一个参数sockfd是函数返回的描述符。
第二个参数是一个指定特定于协议的地址结构的指针。
第三个参数是该地址结构的长度。
监听函数—listen()
listen函数仅由TCP服务器调用,它做两件事。
1. 当socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说,它是一个将调用connect发起连接的客户套接字。listern函数把一个未连接的套接字转换为一个被动套接字,指示内核应接受指向套接字的连接请求。
2. listen的第二个函数规定内核应该为相应套接字排队的最大连接数。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
listen函数通常应该在调用socket和bind这两个函数之后,并在调用accept函数之前调用。
建立连接—accept()和connect()
accept函数由TCP服务器调用,用于从已完成连接队列对头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
参数cliaddr和addrlen用来返回已连接的对端进程的协议地址。如果accept成功,那么其返回值是由内核自动生成的一个全新描述符,代表与所返回客户的TCP连接。
connect函数是由TCP客户端调用,用来建立与TCP服务器的连接。
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
第二个和第三个参数就是连接服务器的套接字地址结构的指针和该结构的大小,用来将客户端的套接字与服务器套接字相连接,所以套接字地址结构必须含有服务器的IP地址和端口号。
这两个函数调用用于完成一个完整相关的建立,其中connect()用于建立连接。无连接的
套接字进程也可以调用connect(),但这时在进程之间没有实际的报文交换,调用将从本地操作系统直接返回。这样做的优点是程序员不必为每一数据指定目的地址,而且如果收到的一个数据报,其目的端口未与任何套接字建立“连接”,便能判断该端口不可操作accept()
用于使服务器等待来自某客户进程的实际连接。
数据传输—send()和recv()
send()调用用于在参数s指定的已连接的数据报或流套接字上发送输出数据,格式如下:
int PASCAL FAR send(SOCKET s, const char FAR *buf, int len, int flags);
参数s为已连接的本地套接字描述符。buf 指向存有发送数据的缓冲区的指针,其长度
由len 指定。flags 指定传输控制方式,如是否发送带外数据等。如果没有错误发生,send()
返回总共发送的字节数。否则它返回SOCKET_ERROR。
recv()调用用于在参数s指定的已连接的数据报或流套接字上接收输入数据,格式如下:
int PASCAL FAR recv(SOCKET s, char FAR *buf, int len, int flags);
参数s 为已连接的套接字描述符。buf指向接收输入数据缓冲区的指针,其长度由len 指
定。flags 指定传输控制方式,如是否接收带外数据等。如果没有错误发生,recv()返回总共
接收的字节数。如果连接被关闭,返回0。否则它返回SOCKET_ERROR。
关闭套接字—close()
通常的UNIX close()函数用来关闭套接字,并终止TCP/UDP连接。
#include <unistd.h>
int close(int sockfd);
close一个TCP套接字的默认行为是把该套接字标记为关闭状态,然后立即返回到调用进程。
五,Socket(TCP / UDP)的代码实现
以下用个不同计算机之间的通信程序来演示Socket的运用,分别以有连接的TCP通信和无连接的UDP通信为例。
TCP如下:
包含的头文件和相关IP端口定义的unp.h文件:
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<pthread.h>
#define SERVER_IP "127.0.0.1" //IP
#define SERVER_PORT 7070 //端口
#define QUEUE_SIZE 5
#define BUFFER_SIZE 256
TCP服务器代码如下:
#include"unp.h"
void* write_fun(void *arg) //写函数
{
int sockConn = *(int *)arg;
char sendbuf[BUFFER_SIZE];
memset(sendbuf, 0, BUFFER_SIZE);
while(1)
{
printf(":>");
scanf("%s",sendbuf);
if(strncmp(sendbuf,"quit",4) == 0)
break;
send(sockConn,sendbuf, strlen(sendbuf)+1, 0);
}
pthread_exit(NULL);
}
void* read_fun(void *arg) //读函数
{
int sockConn = *(int*)arg;
char recvbuf[BUFFER_SIZE];
memset(recvbuf,0,BUFFER_SIZE);
struct sockaddr_in addrCli;
socklen_t addrlen = sizeof(struct sockaddr);
while(1)
{
getsockname(sockConn, (struct sockaddr*)&addrCli, &addrlen);
recv(sockConn,recvbuf,BUFFER_SIZE, 0);
printf("Client[%d]:>%s\n",addrCli.sin_port,recvbuf);
}
}
int main()
{
int sockSer;
sockSer = socket(AF_INET, SOCK_STREAM, 0); //申请套接字
if(sockSer == -1)
perror("socket");
struct sockaddr_in addrSer,addrCli; //定义IP,端口
addrSer.sin_family = AF_INET;
addrSer.sin_port = htons(SERVER_PORT);
addrSer.sin_addr.s_addr = inet_addr(SERVER_IP);
socklen_t addrlen = sizeof(struct sockaddr);
int res = bind(sockSer, (struct sockaddr*)&addrSer, addrlen); //绑定套接字
if(res == -1)
perror("bind");
listen(sockSer, QUEUE_SIZE); //监听函数
int sockConn;
while(1)
{
sockConn = accept(sockSer,(struct sockaddr*)&addrCli, &addrlen); //接收
if(sockConn == -1)
perror("accept");
else
printf("Client[%d] Connect Server OK.\n",addrCli.sin_port);
pthread_t tid[2]; //用线程实现实时写读
pthread_create(&tid[0], NULL, write_fun, &sockConn);
pthread_create(&tid[1], NULL, read_fun, &sockConn);
}
close(sockSer); //关闭套接字
return 0;
}
TCP的客户端代码如下:
#include"unp.h"
pthread_t tid[2];
void* write_fun(void *arg)
{
int sockCli = *(int *)arg;
char sendbuf[BUFFER_SIZE];
memset(sendbuf, 0, BUFFER_SIZE);
while(1)
{
printf(":>");
scanf("%s",sendbuf);
if(strncmp(sendbuf,"quit",4) == 0)
{
pthread_cancel(tid[1]);
break;
}
send(sockCli,sendbuf, strlen(sendbuf)+1, 0);
}
pthread_exit(NULL);
}
void* read_fun(void* arg)
{
int sockCli = *(int*)arg;
char recvbuf[BUFFER_SIZE];
memset(recvbuf,0,BUFFER_SIZE);
struct sockaddr_in addrCli;
socklen_t addrlen = sizeof(struct sockaddr);
getsockname(sockCli, (struct sockaddr*)&addrCli, &addrlen);
while(1)
{
recv(sockCli,recvbuf,BUFFER_SIZE, 0);
printf("Server:>%s\n",recvbuf);
}
}
int main()
{
int sockCli;
sockCli = socket(AF_INET, SOCK_STREAM, 0); //申请客户端的套接字
if(sockCli == -1)
perror("socket");
struct sockaddr_in addrSer;
addrSer.sin_family = AF_INET;
addrSer.sin_port = htons(SERVER_PORT);
addrSer.sin_addr.s_addr = inet_addr(SERVER_IP);
socklen_t addrlen = sizeof(struct sockaddr);
int res = connect(sockCli,(struct sockaddr*)&addrSer, addrlen); //连接服务器
if(res == -1)
perror("connect");
else
{
struct sockaddr_in addrCli;
getsockname(sockCli, (struct sockaddr*)&addrCli, &addrlen);
printf("Client[%d] Connect Server OK.\n", addrCli.sin_port);
}
pthread_create(&tid[0], NULL, write_fun, &sockCli);
pthread_create(&tid[1], NULL, read_fun, &sockCli);
pthread_join(tid[0], NULL);
pthread_join(tid[1], NULL);
close(sockCli);
return 0;
}
无连接的UDP通信服务器代码如下(注意与TCP对比):
#include"unp.h"
int main()
{
int sockSer = socket(AF_INET, SOCK_DGRAM, 0); //创建套接字
if(sockSer == -1)
perror("socket");
struct sockaddr_in addrSer;
addrSer.sin_family = AF_INET;
addrSer.sin_port = htons(5050);
addrSer.sin_addr.s_addr = inet_addr("127.0.0.1");
socklen_t addrlen = sizeof(struct sockaddr);
int res = bind(sockSer,(struct sockaddr*)&addrSer, addrlen);
if(res == -1)
perror("bind");
char sendbuf[256];
char recvbuf[256];
struct sockaddr_in addrCli;
while(1)
{
recvfrom(sockSer,recvbuf,256,0,(struct sockaddr*)&addrCli, &addrlen); //注意无连接的UDP一定要先接收,才能知道客户端的地址
printf("Cli:>%s\n",recvbuf);
printf("Ser:>");
scanf("%s",sendbuf);
sendto(sockSer,sendbuf,strlen(sendbuf)+1,0,(struct sockaddr*)&addrCli, addrlen);
}
return 0;
}
UDP客户端代码:
int main()
{
int sockCli = socket(AF_INET, SOCK_DGRAM, 0);
if(sockCli == -1)
perror("socket");
struct sockaddr_in addrSer;
addrSer.sin_family = AF_INET;
addrSer.sin_port = htons(5050);
addrSer.sin_addr.s_addr = inet_addr("127.0.0.1");
socklen_t addrlen = sizeof(struct sockaddr);
char sendbuf[256];
char recvbuf[256];
struct sockaddr_in addrCli;
while(1)
{
printf("Cli:>");
scanf("%s",sendbuf);
sendto(sockCli,sendbuf,strlen(sendbuf)+1,0,(struct sockaddr*)&addrSer, addrlen);
recvfrom(sockCli,recvbuf,256,0,(struct sockaddr*)&addrSer, &addrlen);
printf("Ser:>%s\n",recvbuf);
}
return 0;
}
Socket的套接字使用就是如上述TCP/UDP通信那样的“套路”,按顺序创建套接字,绑定,监听,接收,发送,关闭。客户端创建套接字,连接,发送,关闭。这些用到的函数是必须掌握的,这是运用Socket的关键。
在此之前,博客内已经介绍了管道通信,进程间的通信,线程间的通信,这些都是”单机“通信。今天介绍的Socket套接字通信实现了不同计算机之间的网络通信,Socket是从单机到网络的转变,Socket是互联网时代通信的基石。
然而Socket高效通信的实现方式任然需要我们认真研究,下一篇博客将介绍高效的Socket通信方式~