socket套接字——流套接字
一、什么是套接字
- socket,即套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。也因为这样,套接字明确地将客户端和服务器区分开来。
二、套接字的属性
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、套接字的协议
- 只要底层的传输机制允许不止一个协议来提供要求的套接字类型,我们就可以为套接字选择一个特定的协议。通常只需要使用默认值。
三、套接字地址
1、AF_UNIX
- 每个套接字都有其自己的地址格式,对于AF_UNIX域套接字来说,它的地址由结构sockaddr_un来描述,该结构定义在头文件sys/un.h中,它的定义如下:
struct sockaddr_un {
sa_family_t sun_family; // AF_UNIX,它是一个短整型
char sum_path[]; // 路径名
};
2、AF_INET
- AF_INET的地址结构由struct sockaddr 结构体来说明描述
struct sockaddr
{
sa_family_t sin_family;//地址族
char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息
};
- 不过这个结构有个缺陷,sa_data将目标地址和端口信息混在一起了,所以我们一般用
struct socket_in来代替
struct sockaddr_in {
short int sin_family;//AF_INET
unsigned short int sin_port;//端口号
struct in_addr sin_addr;//IP地址
unsigned char sin_zero[8];// 这个没有任何作用,只是用来与sockaddr字节对齐
};
struct in_addr{
unsigned long int s_addr;
};
四、基于流套接字的客户/服务器的工作流程
1、服务器
首先服务器应用程序用系统调用socket来创建一个套接字,它是系统分配给该服务器进程的类似文件描述符的资源,
它不能与其他的进程共享。
接下来,服务器进程会给套接字起个名字,我们使用系统调用bind来给套接字命名。
然后服务器进程就开始等待客户连接到这个套接字。
然后,系统调用listen来创建一个队列并将其用于存放来自客户的进入连接。
最后,服务器通过系统调用accept来接受客户的连接。它会创建一个与原有的命名套接不同的新套接字,
这个套接字只用于与这个特定客户端进行通信,而命名套接字(即原先的套接字)则被保留下来继续处理
来自其他客户的连接。
2、客户端
基于socket的客户端比服务器端简单,同样,客户应用程序首先调用socket来创建一个未命名的套接字,
然后将服务器的命名套接字作为一个地址来调用connect与服务器建立连接。
一旦连接建立,我们就可以像使用底层的文件描述符那样用套接字来实现双向数据的通信。
五、流套接字接口
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);
3、listen()系统调用
- 该函数用来创建一个队列来保存未处理的请求。成功时返回0,失败时返回-1
int listen(int socket, int backlog);
- backlog用于指定队列的长度,等待处理的进入连接的个数最多不能超过这个数字,否则往后的连接将被拒绝,导致客户的连接请求失败。调用后,程序一直会监听这个IP端口,如果有连接请求,就把它加入到这个队列中。
5、accept()系统调用
- 该系统调用用来等待客户建立对该套接字的连接。accept系统调用只有当客户程序试图连接到由socket参数指定的套接字上时才返回,也就是说,如果套接字队列中没有未处理的连接,accept将阻塞直到有客户建立连接为止。accept函数将创建一个新套接字来与该客户进行通信,并且返回新套接字的描述符,新套接字的类型和服务器监听套接字类型是一样的。
int accept(int socket, struct sockaddr *address, size_t *address_len);
- address为连接客户端的地址,参数address_len指定客户结构的长度,如果客户地址的长度超过这个值,它将会截断。
6、connect()系统调用
- 该系统调用用来让客户程序通过在一个未命名套接字和服务器监听套接字之间建立连接的方法来连接到服务器。
int connect(int socket, const struct sockaddr *address, size_t address_len);
- 参数socket指定的套接字连接到参数addres指定的服务器套接字。成功时返回0,失败时返回-1.
7、close()系统调用
- 该系统调用用来终止服务器和客户上的套接字连接,我们应该总是在连接的两端(服务器和客户)关闭套接字。
六、示例程序
server.c
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
int server_sockfd = -1;
int client_sockfd = -1;
socklen_t 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);
client_len = sizeof(client_addr);
while (1)
{
char ch = '\0';
printf("Server waiting\n");
// 接收连接,创建新的套接字
client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_addr, &client_len);
if (fork() == 0)
{
// 子进程中,读取客户端发过来的信息,处理信息,再发送给客户端
read(client_sockfd, &ch, 1);
write(client_sockfd, &ch, 1);
sleep(20);
close(client_sockfd);
exit(0);
}
else
{
// 父进程中,关闭套接字, 但是子进程中仍会保留(写时复制)
close(client_sockfd);
}
}
}
client.c
#include <unistd.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
int main(int argc, char **argv)
{
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);
// 连接到服务器
result = connect(sockfd, (struct sockaddr *)&address, (socklen_t)len);
if (result == -1)
{
perror("ops: client\n");
exit(1);
}
// 发送请求给服务器
write(sockfd, &ch, 1);
// 从服务器获取数据
read(sockfd, &ch, 1);
printf("char from server = %c\n", ch);
close(sockfd);
exit(0);
}