更好阅读体验:《理解 TCP 和 UDP》— By Gitbook javascript
咱们已经知道网络中的进程是经过 socket 来通讯的,那什么是 socket 呢?
socket 起源于 UNIX,而 UNIX/Linux 基本哲学之一就是「一切皆文件」,均可以用「open → write/read → close」模式来操做。
socket 其实就是该模式的一个实现,socket 便是一种特殊的文件,一些 socket 函数就是对其进行的操做。 java
使用 TCP/IP 协议的应用程序一般采用系统提供的编程接口:UNIX BSD 的套接字接口(Socket Interfaces)
以此来实现网络进程之间的通讯。
就目前而言,几乎全部的应用程序都是采用 socket,因此说如今的网络时代,网络中进程通讯是无处不在,一切皆 socket c++
套接字接口是一组函数,由操做系统提供,用以建立网络应用。
大多数现代操做系统都实现了套接字接口,包括全部 Unix 变种,Windows 和 Macintosh 系统。 git
套接字接口的起源
套接字接口是加州大学伯克利分校的研究人员在 20 世纪 80 年代早起提出的。
伯克利的研究者使得套接字接口适用于任何底层的协议,第一个实现就是针对 TCP/IP 协议,他们把它包括在 Unix 4.2 BSD 的内核里,而且分发给许多学校和实验室。
这在因特网的历史成为了一个重大事件。
—— 《深刻理解计算机系统》编程
从 Linux 内核的角度来看,一个套接字就是通讯的一个端点。
从 Linux 程序的角度来看,套接字是一个有相应描述符的文件。
普通文件的打开操做返回一个文件描述字,而 socket() 用于建立一个 socket 描述符,惟一标识一个 socket。
这个 socket 描述字跟文件描述字同样,后续的操做都有用到它,把它做为参数,经过它来进行一些操做。 服务器
经常使用的函数有:网络
图中展现了 TCP 协议的 socket 交互流程,描述以下:数据结构
这个过程当中,服务器和客户端创建链接的部分,就体现了 TCP 三次握手的原理。 dom
下面详细讲一下 socket 的各函数。 socket
socket 是系统提供的接口,而操做系统大多数都是用 C/C++ 开发的,天然函数库也是 C/C++ 代码。
该函数会返回一个套接字描述符(socket descriptor),可是该描述符仅是部分打开的,还不能用于读写。
如何完成打开套接字的工做,取决于咱们是客户端仍是服务器。
#include <sys/socket.h>
int socket(int domain, int type, int protocol);复制代码
domain:
协议域,决定了 socket 的地质类型,在通讯中必须采用对应的地址。
经常使用的协议族有:AF_INET
(ipv4地址与端口号的组合)、AF_INET6
(ipv6地址与端口号的组合)、AF_LOCAL
(绝对路径名做为地址)。
该值的常量定义在 sys/socket.h
文件中。
type:
指定 socket 类型。
经常使用的类型有:SOCK_STREAM
、SOCK_DGRAM
、SOCK_RAW
、SOCK_PACKET
、SOCK_SEQPACKET
等。
其中 SOCK_STREAM
表示提供面向链接的稳定数据传输,即 TCP 协议。
该值的常量定义在 sys/socket.h
文件中。
protocol:
指定协议。
经常使用的协议有:IPPROTO_TCP
(TCP协议)、IPPTOTO_UDP
(UDP协议)、IPPROTO_SCTP
(STCP协议)。
当值位 0 时,会自动选择 type
类型对应的默认协议。
由服务端调用,把一个地址族中的特定地址和 socket 联系起来。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);复制代码
sockfd:
即 socket 描述字,由 socket() 函数建立。
*addr:
一个 const struct sockaddr
指针,指向要绑定给 sockfd
的协议地址。
这个地址结构根据地址建立 socket 时的地址协议族不一样而不一样,例如 ipv4 对应 sockaddr_in
,ipv6 对应 sockaddr_in6
.
这几个结构体在使用的时候,均可以强制转换成 sockaddr
。
下面是这几个结构体对应的所在的头文件:
sockaddr
: sys/socket.h
sockaddr_in
: netinet/in.h
sockaddr_in6
: netinet6/in.h
_in 后缀意义:互联网络(internet)的缩写,而不是输入(input)的缩写。
服务器调用,将 socket 从一个主动套接字转化为一个监听套接字(listening socket), 该套接字能够接收来自客户端的链接请求。
在默认状况下,操做系统内核会认为 socket 函数建立的描述符对应于主动套接字(active socket)。
#include <sys/socket.h>
int listen(int sockfd, int backlog);复制代码
sockfd:
即 socket 描述字,由 socket() 函数建立。
backlog:
指定在请求队列中的最大请求数,进入的链接请求将在队列中等待 accept() 它们。
由客户端调用,与目的服务器的套接字创建一个链接。
#include <sys/socket.h>
int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);复制代码
clientfd:
目的服务器的 socket 描述符
*addr:
一个 const struct sockaddr
指针,包含了目的服务器 IP 和端口。
addrlen:
协议地址的长度,若是是 ipv4 的 TCP 链接,通常为 sizeof(sockaddr_in)
;
服务器调用,等待来自客户端的链接请求。
当客户端链接,accept 函数会在 addr
中会填充上客户端的套接字地址,而且返回一个已链接描述符(connected descriptor),这个描述符能够用来利用 Unix I/O 函数与客户端通讯。
#indclude <sys/socket.h>
int accept(int listenfd, struct sockaddr *addr, int *addrlen);复制代码
listenfd:
服务器的 socket 描述字,由 socket() 函数建立。
*addr:
一个 const struct sockaddr
指针,用来存放提出链接请求客户端的主机的信息
*addrlen:
协议地址的长度,若是是 ipv4 的 TCP 链接,通常为 sizeof(sockaddr_in)
。
在数据传输完成以后,手动关闭链接。
#include <sys/socket.h>
#include <unistd.h>
int close(int fd);复制代码
fd:
须要关闭的链接 socket 描述符
当客户端和服务器创建链接后,可使用网络 I/O 进行读写操做。
网络 I/O 操做有下面几组:
最经常使用的是 read()/write()
他们的原型是:
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);复制代码
鉴于该文是侧重于描述 socket 的工做原理,就再也不详细描述这些函数了。
// socket_server.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#define MAXLINE 4096 // 4 * 1024
int main(int argc, char **argv) {
int listenfd, // 监听端口的 socket 描述符
connfd; // 链接端 socket 描述符
struct sockaddr_in servaddr;
char buff[MAXLINE];
int n;
// 建立 socket,而且进行错误处理
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
// 初始化 sockaddr_in 数据结构
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(6666);
// 绑定 socket 和 端口
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1)
{
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
// 监听链接
if (listen(listenfd, 10) == -1)
{
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
printf("====== Waiting for client's request======\n");
// 持续接收客户端的链接请求
while (true)
{
if ((connfd = accept(listenfd, (struct sockaddr *)NULL, NULL) == -1))
{
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
continue;
}
n = recv(connfd, buff, MAXLINE, 0);
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
close(connfd);
}
close(listenfd);
return 0;
}复制代码
// socket_client.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define MAXLINE 4096
int main(int argc, char **argv) {
int sockfd, n;
char recvline[4096], sendline[4096];
struct sockaddr_in servaddr;
if (argc != 2)
{
printf("usage: ./client <ipaddress>\n");
return 0;
}
// 建立 socket 描述符
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
// 初始化目标服务器数据结构
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(6666);
// 从参数中读取 IP 地址
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
{
printf("inet_pton error for %s\n", argv[1]);
return 0;
}
// 链接目标服务器,并和 sockfd 联系起来。
if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
{
printf("connect error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
printf("send msg to server: \n");
// 从标准输入流中读取信息
fgets(sendline, 4096, stdin);
// 经过 sockfd,向目标服务器发送信息
if (send(sockfd, sendline, strlen(sendline), 0) < 0)
{
printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
// 数据传输完毕,关闭 socket 链接
close(sockfd);
return 0;
}复制代码
首先建立 makefile
文件
all:server client
server:socket_server.o
g++ -g -o socket_server socket_server.o
client:socket_client.o
g++ -g -o socket_client socket_client.o
socket_server.o:socket_server.cpp
g++ -g -c socket_server.cpp
socket_client.o:socket_client.cpp
g++ -g -c socket_client.cpp
clean:all
rm all复制代码
而后使用命令:
$ make复制代码
会生成两个可执行文件:
socket_server
socket_client
分别打开两个终端,运行:
./socket_server
./socket_client 127.0.0.1
而后在 socket_client
中键入发送内容,能够再 socket_server
接收到一样的信息。