socket是什么?套接字是什么?
什么是 socket?
socket 的原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。
我们把插头插到插座上就能从电网获得电力供应,同样,为了与远程计算机进行数据传输,需要连接到因特网,而 socket 就是用来连接到因特网的工具。
socket 的典型应用就是 Web 服务器和浏览器:浏览器获取用户输入的 URL,向服务器发起请求,服务器分析接收到的 URL,将对应的网页内容返回给浏览器,浏览器再经过解析和渲染,就将文字、图片、视频等元素呈现给用户。
学习 socket,也就是学习计算机之间如何通信,并编写出实用的程序。
UNIX/Linux 中的 socket 是什么?
在 UNIX/Linux 系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。
你也许听很多高手说过,UNIX/Linux 中的一切都是文件!那个家伙说的没错。
为了表示和区分已经打开的文件,UNIX/Linux 会给每个文件分配一个 ID,这个 ID 就是一个整数,被称为文件描述符(File Descriptor)。例如:
- 通常用 0 来表示标准输入文件(stdin),它对应的硬件设备就是键盘;
- 通常用 1 来表示标准输出文件(stdout),它对应的硬件设备就是显示器。
UNIX/Linux 程序在执行任何形式的 I/O 操作时,都是在读取或者写入一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接。
请注意,网络连接也是一个文件,它也有文件描述符!你必须理解这句话。
我们可以通过 socket() 函数来创建一个网络连接,或者说打开一个网络文件,socket() 的返回值就是文件描述符。有了文件描述符,我们就可以使用普通的文件操作函数来传输数据了,例如:
- 用 read() 读取从远程计算机传来的数据;
- 用 write() 向远程计算机写入数据。
你看,只要用 socket() 创建了连接,剩下的就是文件操作了,网络编程原来就是如此简单!
IP地址
IP地址是 Internet Protocol Address 的缩写,译为“网际协议地址”。
目前大部分软件使用 IPv4 地址,但 IPv6 也正在被人们接受,尤其是在教育网中,已经大量使用。
一台计算机可以拥有一个独立的 IP 地址,一个局域网也可以拥有一个独立的 IP 地址(对外就好像只有一台计算机)。对于目前广泛使用 IPv4 地址,它的资源是非常有限的,一台计算机一个 IP 地址是不现实的,往往是一个局域网才拥有一个 IP 地址。
在因特网上进行通信时,必须要知道对方的 IP 地址。实际上数据包中已经附带了 IP 地址,把数据包发送给路由器以后,路由器会根据 IP 地址找到对方的地里位置,完成一次数据的传递。路由器有非常高效和智能的算法,很快就会找到目标计算机。
MAC地址
现实的情况是,一个局域网往往才能拥有一个独立的 IP;换句话说,IP 地址只能定位到一个局域网,无法定位到具体的一台计算机。这可怎么办呀?这样也没法通信啊。
其实,真正能唯一标识一台计算机的是 MAC 地址,每个网卡的 MAC 地址在全世界都是独一无二的。计算机出厂时,MAC 地址已经被写死到网卡里面了(当然通过某些“奇巧淫技”也是可以修改的)。局域网中的路由器/交换机会记录每台计算机的 MAC 地址。
MAC 地址是 Media Access Control Address 的缩写,直译为“媒体访问控制地址”,也称为局域网地址(LAN Address),以太网地址(Ethernet Address)或物理地址(Physical Address)。
数据包中除了会附带对方的 IP 地址,还会附带对方的 MAC 地址,当数据包达到局域网以后,路由器/交换机会根据数据包中的 MAC 地址找到对应的计算机,然后把数据包转交给它,这样就完成了数据的传递。
端口号
有了 IP 地址和 MAC 地址,虽然可以找到目标计算机,但仍然不能进行通信。一台计算机可以同时提供多种网络服务,例如 Web 服务(网站)、FTP 服务(文件传输服务)、SMTP 服务(邮箱服务)等,仅有 IP 地址和 MAC 地址,计算机虽然可以正确接收到数据包,但是却不知道要将数据包交给哪个网络程序来处理,所以通信失败。
为了区分不同的网络程序,计算机会为每个网络程序分配一个独一无二的端口号(Port Number),例如,Web 服务的端口号是 80,FTP 服务的端口号是 21,SMTP 服务的端口号是 25。
端口(Port)是一个虚拟的、逻辑上的概念。可以将端口理解为一道门,数据通过这道门流入流出,每道门有不同的编号,就是端口号。
socket()创建套接字,成功返回套接字的文件描述符,失败返回-1
domain: 设置套接字的协议簇, AF_UNIX AF_INET AF_INET6
type: 设置套接字的服务类型 SOCK_STREAM SOCK_DGRAM
protocol: 一般设置为 0,表示使用默认协议
bind是服务端调用的函数,绑定的是服务端的IP,假设服务端有多个网卡,即会有多个IP,这个时候我们可以选择用bind绑定其中一个IP,那么服务端只接收该IP上某端口的数据。
listen()创建一个监听队列以存储待处理的客户连接,成功返回 0,失败返回-1
sockfd 是被监听的 socket 套接字
backlog 表示处于完全连接状态的 socket 的上限
accept()从 listen 监听队列中接收一个连接,成功返回一个新的连接 socket,
该 socket 唯一地标识了被接收的这个连接,失败返回-1
sockfd 是执行过 listen 系统调用的监听 socket
addr 参数用来获取被接受连接的远端 socket 地址
addrlen 指定该 socket 地址的长度
connect()客户端需要通过此系统调用来主动与服务器建立连接, 成功返回 0,失败返回-1
sockfd 参数是由 socket()返回的一个 socket。
serv_addr 是服务器监听的 socket 地址
addrlen 则指定这个地址的长度
socket()方法是用来创建一个套接字,有了套接字就可以通过网络进行数据的收发。这也 是为什么进行网络通信的程序首先要创建一个套接字。创建套接字时要指定使用的服务类 型,使用 TCP 协议选择流式服务(SOCK_STREAM)。
bind()方法是用来指定套接字使用的 IP 地址和端口。IP 地址就是自己主机的地址,如果 主机没有接入网络,测试程序时可以使用回环地址“127.0.0.1”。端口是一个 16 位的整形值, 一般 0-1024 为知名端口,如 HTTP 使用的 80 号端口。这类端口一般用户不能随便使用。其 次,1024-4096 为保留端口,用户一般也不使用。4096 以上为临时端口,用户可以使用。在 Linux 上,1024 以内的端口号,只有 root 用户可以使用。
listen()方法是用来创建监听队列。监听队列有两种,一个是存放未完成三次握手的连接, 一种是存放已完成三次握手的连接。listen()第二个参数就是指定已完成三次握手队列的长度。
accept()处理存放在 listen 创建的已完成三次握手的队列中的连接。每处理一个连接,则 accept()返回该连接对应的套接字描述符。如果该队列为空,则 accept 阻塞。
connect()方法一般由客户端程序执行,需要指定连接的服务器端的 IP 地址和端口。该方 法执行后,会进行三次握手, 建立连接。
send()方法用来向 TCP 连接的对端发送数据。send()执行成功,只能说明将数据成功写入 到发送端的发送缓冲区中,并不能说明数据已经发送到了对端。send()的返回值为实际写入 到发送缓冲区中的数据长度。
recv()方法用来接收 TCP 连接的对端发送来的数据。recv()从本端的接收缓冲区中读取数 据,如果接收缓冲区中没有数据,则 recv()方法会阻塞。返回值是实际读到的字节数,如果 recv()返回值为 0, 说明对方已经关闭了 TCP 连接。
close()方法用来关闭 TCP 连接。此时,会进行四次挥手。
Linux下代码
服务器端:
#include <stdio.h>
2. #include <stdlib.h>
3. #include <unistd.h>
4. #include <string.h>
5. #include <assert.h>
6. #include <sys/socket.h>
7. #include <sys/types.h>
8. #include <arpa/inet.h>
9. #include <netinet/in.h>
10.
11. int main()
12. {
13. int sockfd = socket(AF_INET, SOCK_STREAM, 0);
14. assert( sockfd != -1 );
15.
16.struct sockaddr_in saddr;
17. memset( &saddr, 0, sizeof(saddr) );
18. saddr.sin_family = AF_INET;
19. saddr.sin_port = htons(6000); // htons 将主机字节序转换为网络字节
序
20. saddr.sin_addr.s_addr = inet_addr( "127.0.0.1" ); // 回环地址
21.
22. int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
23. assert( res != -1 );
24.
25. res = listen(sockfd, 5);
26. assert( res != -1 );
27.
28. while( 1 ) // 服务器循环接收客户端连接
29. {
30. struct sockaddr_in caddr;
31. socklen_t len = sizeof(caddr);
32. int c = accept( sockfd, (struct sockaddr*)&caddr, &len ); // 阻塞
33. if(c == -1)
34. {
35. perror("accept error ");
36. continue;
37. }
38.
39. printf("accept c = %d\n", c);
40. char data[128] = {0};
41. int n = recv(c, data, 127, 0); // 阻塞
42.
43. printf("n = %d, buff = %s\n", n, data);
44.
45. send(c, "OK", 2, 0);
46.
47. close(c);
48. }
49.
50. close(sockfd);
51.
52. exit(0);
53. }
客户端:
1. #include <stdio.h>
2. #include <stdlib.h>
3. #include <unistd.h>
4. #include <string.h>
5. #include <assert.h>
6. #include <sys/socket.h>
7. #include <sys/types.h>
8. #include <arpa/inet.h>
9. #include <netinet/in.h>
10.
11. int main()
12. {
13. int sockfd = socket(AF_INET, SOCK_STREAM, 0);
14. assert(sockfd != -1);
15.
16. struct sockaddr_in saddr;
17. memset(&saddr, 0, sizeof(saddr));
18. saddr.sin_family = AF_INET;
19. saddr.sin_port = htons(6000);
20. saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
21.
22. int res = connect(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
23. assert(res != -1);
24.
25. printf("please input: ");
26. char buff[128] = {0};
27. fgets(buff, 128, stdin);
28.
29. send(sockfd, buff, strlen(buff) - 1, 0);
30.
31. char data[128] = {0};
32. int n = recv(sockfd, data, 127, 0);
33.
34. printf("%s\n", data);
35.
36. close(sockfd);
37.
38. exit(0);
39. }
inet_addr()作用是将一个IP字符串转化为一个网络字节序的整数值,用于sockaddr_in.sin_addr.s_addr。
inet_ntoa()作用是将一个sin_addr结构体输出成IP字符串(network to ascii)
htonl()函数
函数原型是:uint32_t htonl(uint32_t hostlong)
其中,hostlong是主机字节顺序表达的32位数,htonl中的h–host主机地址,to–to,n–net网络,l–unsigned long无符号的长整型(32位的系统是4字节);
函数返回值是一个32位的网络字节顺序;
函数的作用是将一个32位数从主机字节顺序转换成网络字节顺序。
htons()函数
函数原型是:uint16_t htons(uint16_t hostlong)
其中,hostlong是主机字节顺序表达的16位数,htons中的h–host主机地址,to–to,n–net网络,s–signed long无符号的短整型(32位的系统是2字节);
函数返回值是一个16位的网络字节顺序;
函数的作用是将一个16位数从主机字节顺序转换成网络字节顺序,简单的说就是把一个16位数高低位呼唤。
ntohs()函数
函数原型是:uint16_t ntohs(uint16_t hostlong)
其中,hostlong是网络字节顺序表达的16位数,ntohs中的,n–net网络,to–toh–host主机地址,s–signed long有符号的短整型(32位的系统是2字节);
函数返回值是一个16位的主机字节顺序;
函数的作用是将一个16位数由网络字节顺序转换为主机字节顺序,简单的说就是把一个16位数高低位互换。
ntohl()函数
函数原型是:uint32_t ntohs(uint32_t hostlong)
其中,hostlong是网络字节顺序表达的32位数,ntohs中的,n–net网络,to–toh–host主机地址,s–unsigned long无符号的短整型(32位的系统是4字节);
函数返回值是一个32位的主机字节顺序;
函数的作用是将一个32位数由网络字节顺序转换为主机字节顺序。
这些函数存在的意义
为什么存在这个函数呢?或者存在这个函数的意义?
说到这部分需要引入字节存放的两个概念一个是“大端顺序”,一个是“小端顺序”。俗称“小尾顺序”、“大尾顺序”。
简单的说就是对应数据的高字节存放在低地址,低字节存放在高地址上就是大端顺序,对应数据的高字节存放在高地址,低字节存放在低地址上就是小端顺序。
比如 unsigned long hostlong = 0xa2b4c6d8;
大端顺序存放:
偏移地址 存放内容
0x00000000 0xa2
0x00000001 0xb4
0x00000002 0xc6
0x00000003 0xd8
小端顺序存放:
偏移地址 存放内容
0x00000000 0xd8
0x00000001 0xc6
0x00000002 0xb4
0x00000003 0xa2
同理推理16位数以及64位数。
一般地,在我的编译器里面设置的是小端顺序,这个可以根据自己的编译器看下设置,这里不深入说明。但是网络传输数据采用的是大端顺序。所以这才涉及到主机字节顺序和网络字节顺序,再说的详细一点,主机字节顺序可能是大端顺序或者小端顺序(这个要看编译器的设置,还有自己是用的C还是Java还是其他的语言,其各自都是不尽相同),但是网络字节顺序一定是大端顺序。
多说几句
为什么会有这个大端模式和小端模式呢?
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为 8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于 8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。例如一个16bit的short型x,在内存中的地址为0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于 大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。小端模式,刚好相反。我们常用的X86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以随时在程序中(在ARM Cortex 系列使用REV、REV16、REVSH指令[1] )进行大小端的切换。
多进程、多线程处理并发 如下图所示, 当一个客户端与服务器建立连接以后,服务器端 accept()返回,进而准备 循环接收客户端发过来的数据。如果客户端暂时没发数据,服务端会在第 40 行的 recv()阻 塞。此时,其他客户端向服务器发起连接后,由于服务器阻塞了,无法执行 accept()接受连 接,也就是其他客户端发送的数据,服务器无法读取。服务器也就无法并发同时处理多个客 户端
这个问题可以通过引入多线程和多进程来解决。服务端接受一个客户端的连接后,创建 一个线程或者进程,然后在新创建的线程或进程中循环处理数据。主线程(父进程)只负责 监听客户端的连接,并使用 accept()接受连接,不进行数据的处理。如下图所示:
服务端:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<signal.h>
#include<sys/wait.h>
void DealClientLink(int c,struct sockaddr_in caddr)
{
while(1)
{
char buff[128] = {0};
int n = recv(c,buff,127,0);
if(n <= 0)
{
break;
}
printf("%s:%d %s\n",inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port),buff);
send(c,"OK",2,0);
}
}
void sigfun(int sign)
{
wait(NULL);
}
int main()
{
signal(SIGCHLD,sigfun);
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
assert(res != -1);
listen(sockfd,5);
while(1)
{
struct sockaddr_in caddr;
socklen_t len = sizeof(caddr);
int c = accept(sockfd,(struct sockaddr*)&caddr,&len);
assert(-1 != c);
printf("%s:%d Link Succes\n",inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));
pid_t pid = fork();
assert(-1 != pid);
if(0 == pid)
{
DealClientLink(c,caddr);
exit(0);
}
else
{
close(c);
}
}
close(sockfd);
exit(0);
}
客户端:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
assert(res != -1);
while(1)
{
char buff[128] = {0};
printf("input:\n");
fgets(buff,128,stdin);
//如果buff==end
if(strncmp(buff,"end",3) == 0)
{
break;
}
send(sockfd,buff,strlen(buff),0);
memset(buff,128,0);
recv(sockfd,buff,127,0);
printf("buff = %s\n",buff);
}
close(sockfd);
}