进程间的通信—套接字(socket)
  前⾯说到的进程间的通信,所通信的进程都是在同⼀台计算机上的,⽽使⽤socket进⾏通信的进程可以是同⼀台计算机的进程,也是可以是通过⽹络连接起来的不同计算机上的进程。通常我们使⽤socket进⾏⽹络编程,这⾥将会简单地讲述如何使⽤socket进⾏简单的⽹络编程。
⼀、什么是socket
  socket,即套接字是⼀种通信机制,凭借这种机制,客户/服务器(即要进⾏通信的进程)系统的开发⼯作既可以在本地单机上进⾏,也可以跨⽹络进⾏。也就是说它可以让不在同⼀台计算机但通过⽹络连接计算机上的进程进⾏通信。也因为这样,套接字明确地将客户端和服务器区分开来。
⼆、套接字的属性
套接字的特性由3个属性确定,它们分别是:域、类型和协议。
1、套接字的域
  它指定套接字通信中使⽤的⽹络介质,最常见的套接字域是AF_INET,它指的是Internet⽹络。当客户使⽤套接字进⾏跨⽹络的连接时,它就需要⽤到服务器计算机的IP地址和端⼝来指定⼀台联⽹机器上的某个
特定服务,所以在使⽤socket作为通信的终点,服务器应⽤程序必须在开始通信之前绑定⼀个端⼝,服务器在指定的端⼝等待客户的连接。另⼀个域AF_UNIX表⽰UNIX⽂件系统,它就是⽂件输⼊/输出,⽽它的地址就是⽂件名。
2、套接字类型
  因特⽹提供了两种通信机制:流(stream)和数据报(datagram),因⽽套接字的类型也就分为流套接字和数据报套接字。这⾥主要讲流套接字。
  流套接字由类型SOCK_STREAM指定,它们是在AF_INET域中通过TCP/IP连接实现,同时也是AF_UNIX中常⽤的套接字类型。流套接字提供的是⼀个有序、可靠、双向字节流的连接,因此发送的数据可以确保不会丢失、重复或乱序到达,⽽且它还有⼀定的出错后重新发送的机制。
  与流套接字相对的是由类型SOCK_DGRAM指定的数据报套接字,它不需要建⽴连接和维持⼀个连接,它们在AF_INET中通常是通
过UDP/IP协议实现的。它对可以发送的数据的长度有限制,数据报作为⼀个单独的⽹络消息被传输,它可能会丢失、复制或错乱到达,UDP不是⼀个可靠的协议,但是它的速度⽐较⾼,因为它并⼀需要总是要建⽴和维持⼀个连接。
3、套接字协议
  只要底层的传输机制允许不⽌⼀个协议来提供要求的套接字类型,我们就可以为套接字选择⼀个特定的协议。通常只需要使⽤默认值。
三、套接字地址
  每个套接字都有其⾃⼰的地址格式,对于AF_UNIX域套接字来说,它的地址由结构sockaddr_un来描述,该结构定义在头⽂件sys/un.h中,它的定义如下:
struct sockaddr_un{
sa_family_t sun_family;//AF_UNIX,它是⼀个短整型
char        sum_path[];//路径名
};
对于AF_INET域套接字来说,它的地址结构由sockaddr_in来描述,它⾄少包括以下⼏个成员:
struct sockaddr_in{
short int            sin_family;//AF_INET
unsigned short int    sin_port;//端⼝号
struct in_addr        sin_addr;//IP地址
};
⽽in_addr被定义为:
[cpp] view plain copy
struct in_addr{
unsigned long int s_addr;
};
四、基于流套接字的客户/服务器的⼯作流程
  使⽤socket进⾏进程通信的进程采⽤的客户/服务器系统是如何⼯作的呢?
1、服务器端
  ⾸先服务器应⽤程序⽤系统调⽤socket来创建⼀个套接字,它是系统分配给该服务器进程的类似⽂件描述符的资源,它不能与其他的进程共享。
  接下来,服务器进程会给套接字起个名字,我们使⽤系统调⽤bind来给套接字命名。然后服务器进程就开始等待客户连接到这个套接字。
  然后,系统调⽤listen来创建⼀个队列并将其⽤于存放来⾃客户的进⼊连接。
  最后,服务器通过系统调⽤accept来接受客户的连接。它会创建⼀个与原有的命名套接不同的新套接字,这个套接字只⽤于与这个特定客户端进⾏通信,⽽命名套接字(即原先的套接字)则被保留下来继续处理来⾃其他客户的连接。
2、客户端
  基于socket的客户端⽐服务器端简单,同样,客户应⽤程序⾸先调⽤socket来创建⼀个未命名的套接字,然后将服务器的命名套接字作为⼀个地址来调⽤connect与服务器建⽴连接。
  ⼀旦连接建⽴,我们就可以像使⽤底层的⽂件描述符那样⽤套接字来实现双向数据的通信。
五、流式socket的接⼝及作⽤
  socket的接⼝函数声明在头⽂件sys/types.h和sys/socket.h中。
1、创建套接字——socket系统调⽤
  该函数⽤来创建⼀个套接字,并返回⼀个描述符,该描述符可以⽤来访问该套接字,它的原型如下:
int socket(int domain, int type, int protocol);
函数中的三个参数分别对应前⾯所说的三个套接字属性。
protocol参数设置为0表⽰使⽤默认协议。
2、命名(绑定)套接字——bind系统调⽤
  该函数把通过socket调⽤创建的套接字命名,从⽽让它可以被其他进程使⽤。对于AF_UNIX,调⽤该函数后套接字就会关联到⼀个⽂件系统路径名,对于AF_INET,则会关联到⼀个IP端⼝号。函数原型如下:
int bind( int socket, const struct sockaddr *address, size_t address_len);
成功时返回0,失败时返回-1;
3、创建套接字队列(监听)——listen系统调⽤
  该函数⽤来创建⼀个队列来保存未处理的请求。成功时返回0,失败时返回-1,其原型如下:
int listen(int socket, int backlog);
backlog⽤于指定队列的长度,等待处理的进⼊连接的个数最多不能超过这个数字,
否则往后的连接将被拒绝,导致客户的连接请求失败。
调⽤后,程序⼀直会监听这个IP端⼝,如果有连接请求,就把它加⼊到这个队列中。
4、接受连接——accept系统调⽤
  该系统调⽤⽤来等待客户建⽴对该套接字的连接。accept系统调⽤只有当客户程序试图连接到由socket参数指定的套接字上时才返回,也就是说,如果套接字队列中没有未处理的连接,accept将阻塞直到有客户建⽴连接为⽌。accept函数将创建⼀个新套接字来与该客户进⾏通信,并且返回新套接字的描述符,新套接字的类型和服务器监听套接字类型是⼀样的。它的原型如下:
int accept(int socket, struct sockaddr *address, size_t *address_len);
address为连接客户端的地址,
参数address_len指定客户结构的长度,如果客户地址的长度超过这个值,它将会截断。
5、请求连接——connect系统调⽤
  该系统调⽤⽤来让客户程序通过在⼀个未命名套接字和服务器监听套接字之间建⽴连接的⽅法来连接到服务器。它的原型如下:
int connect(int socket, const struct sockaddr *address, size_t address_len);
参数socket指定的套接字连接到参数addres指定的服务器套接字。
成功时返回0,失败时返回-1.
6、关闭socket——close系统调⽤
  该系统调⽤⽤来终⽌服务器和客户上的套接字连接,我们应该总是在连接的两端(服务器和客户)关闭套接字。
六、进程使⽤流式socket进⾏通信
  下⾯⽤多个客户程序和⼀个服务器程序来展⽰进程间如何利⽤套接字进⾏通信。
  sockserver.c是⼀个服务器程序,它⾸先创建套接字,然后绑定⼀个端⼝再监听套接字,忽略⼦进程的停⽌消息等,然后它进⼊循环,⼀直循环检查是否有客户连接到服务器,如果有,则调⽤fork()创建⼀个⼦进程来处理请求。利⽤read系统调⽤来读取客户端发来的信息,利
⽤write系统调⽤来向客户端发送信息。这个服务器的⼯作⾮常简单,就是把客户发过来的字符+1,再发送回给客户。
  sockclient.c是⼀个客户程序,它同样要先创建套接,然后连接到指定IP端⼝服务器,如果连接成功,就⽤write来发送信息给服务器,再⽤read获取服务器处理后的信息,再输出。
服务器sockserver.c的源代码如下:
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
int server_sockfd = -1;
int client_sockfd = -1;
int client_len = 0;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
/
/创建流套接字
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
//设置服务器接收的连接地址和监听的端⼝
server_addr.sin_family = AF_INET;//指定⽹络套接字
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//接受所有IP地址的连接
server_addr.sin_port = htons(9736);//绑定到9736端⼝
//绑定(命名)套接字
bind(server_sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
//创建套接字队列,监听套接字
listen(server_sockfd, 5);
//忽略⼦进程停⽌或退出信号
signal(SIGCHLD, SIG_IGN);
while(1)
{
char ch = '\0';
client_len = sizeof(client_addr);
printf("Server waiting\n");
//接受连接,创建新的套接字
client_sockfd = accept(server_sockfd, (struct sockaddr*)&client_addr, &client_len);
if(fork() == 0)
{
//⼦进程中,读取客户端发过来的信息,处理信息,再发送给客户端
read(client_sockfd, &ch, 1);
sleep(5);
ch++;
write(client_sockfd, &ch, 1);
close(client_sockfd);
exit(0);
}
else
{
//⽗进程中,关闭套接字
close(client_sockfd);
}
}
}
客户sockclient.c的源代码如下:
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
int sockfd = -1;
int len = 0;
struct sockaddr_in address;
int result;
char ch = 'A';
//创建流套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
//设置要连接的服务器的信息
address.sin_family = AF_INET;//使⽤⽹络套接字
address.sin_addr.s_addr = inet_addr("127.0.0.1");//服务器地址
address.sin_port = htons(9736);//服务器所监听的端⼝
len = sizeof(address);
//连接到服务器socket通信在哪一层
result = connect(sockfd, (struct sockaddr*)&address, len);
if(result == -1)
{
perror("ops:client\n");
exit(1);
}
//发送请求给服务器
write(sockfd, &ch, 1);
//从服务器获取数据
read(sockfd, &ch, 1);
printf("char form server = %c\n", ch);
close(sockfd);
exit(0);
}
运⾏结果如下:
  在本例⼦中,我们启动了⼀个服务器程序和三个客户程序,从运⾏的结果来看,客户端发送给服务器程序的所有请求都得到了处理,即把A变成了B。对于服务器和客户程序之间使⽤的read和write系统调⽤跟使⽤命名管道时阻塞的read、write系统调⽤⼀样。例如客户程序调
⽤read时,如果服务器程序没有向指定的客户程序的socket中写⼊信息,则read调⽤会⼀直阻塞。
七、流式套接字给我印象
  给我的感觉是流式套接字很像命名管道,但是它却可以使不在同⼀台计算机⽽通过⽹络连接的不同计算机上的进程进⾏通信,功能真是⾮常的强⼤。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。