一、套接字的概念
一个套接字是网络连接的一个端点。每个套接字都有相应的套接字地址,由一个32位的因特网地址和一个16位的端口号组成,用“IP地址:端口号”来表示,如:192.168.181.129:9999,其中“192.168.181.129”表示IP地址,“9999”表示端口号。
二、预备知识
1、认识IP地址
(1)IP协议有两个版本,IPv4和IPv6,现在用得比较多的是IPv4。
(2)IP地址是在IP协议中用来标识网络中不同主机的地址。
(3)对于IPv4版本来说,IP地址是一个4字节,32位的整数。
(4)通常使用“点分十进制”的字符串表示IP地址,例如:“192.168.181.129”,其中用点分割的每一个数字表示一个字节,范围为0-255。
(5)在IP数据报头部,有两个IP地址,分别叫做源IP地址和目的IP地址。
有了IP地址能够把信息发送到对方的机器上,但是还需要一个其他的标识符来区分这个数据要给哪个程序进行解析,它就是端口号。
2、认识端口号
(1)端口号是具有网络功能的应用软件的标识号。
(2)端口号用来标识一个进程,告诉操作系统当前的这个数据要交给哪一个进程来处理。
(3)端口号是一个2字节,16位的整数,可以标识的范围是0-65535。其中,0-1023是公认端口号,即已经公认定义或为将要公认定义的软件保留的,而1024-65535是并没有公共定义的端口号,用户可以自己定义这些端口的作用。
(4)IP地址+端口号能够标识网络上的某一台主机的某一个进程。
(5)一个进程能够占用多个端口号,但是一个端口号只能被一个进程占用。
三、套接字接口
套接字接口是一组函数,用以创建网络应用,存放在“sys/socket.h”函数库中。
1、socket函数
(1)函数功能:创建一个套接字描述符。
(2)函数原型:int socket(int demain, int type, int protocol)
(3)参数:
1)domain:地址域(版本号IPV4),通常使用AF_INET表示32位IP地址。
2)type:套接字类型,SOCK_STREAM表字节流类型(TCP);SOCK_DGRAM表数据报类型(UDP)。
3)protocol:协议,IPPROTO_TCP表示TCP协议;IPPROTO_UDP表示UDP协议;“0”表示接受任何协议。
(4)返回值:若成功返回0,失败返回-1。
(5)释:socket函数旨在打开一个网络通讯端口,如果成功就像open一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据。
2、bind函数
(1)函数功能:将套接字地址和套接字描述符联系起来。
(2)函数原型:int bind(int sockfd, const struct sockaddr *serveraddr, socklen_t addrlen)。
(3)参数:
1)sockfd:套接字描述符。
2)addr:套接字的地址。
3)addlen:IPv4结构体的大小,即sizeof(sockaddr_in)。
(4)返回值:成功返回0,失败返回-1。
(5)初始化:
struct sockaddr_in serveraddr
Bzero(&serveraddr, sizeof(serveraddr));
Serveraddr.sin_family = AF_INET;
Serveraddr.sin_port = htons(SERV_PORT);
Serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
1)bzero表示将整个结构体清零。
2)网络地址为INADDR_ANY表示本地的任意IP地址。因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到某个客户端建立了连接时才确定下来到底用哪个IP地址。
3)端口号SERV_PORT在这里定义为9999。
(6)释:
1)服务器程序所监听的网络端口号和网络地址通常是固定不变,客户端得知服务器程序的地址和端口号后就可以向服务器发起连接请求。所以服务器需要绑定一个固定的网络地址和端口号。
2)“struct sockaddr*”是通用指针类型,实际上serveraddr参数可以接受多种协议的sockaddr结构体,所以需要第三个参数addrlen指定结构体的长度。
(7)注意:
1)客户端不是不允许调用bind函数,只是没有必要调用bind函数固定一个端口。否则,如果在同一台机器上启动多个客户端,就会出现端口号被占用导致不能正确建立连接。
2)服务器也不是必须调用bind函数,但是如果服务器不调用bind函数,内核就会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。
3、listen函数
(1)函数功能:服务器告知内核,套接字被服务器使用了,转化为监听套接字,接受客户端的连接请求。
(2)函数原型:int listen(int sockfd, int backlog)。
(3)参数:
1)sockfd:套接字描述符。
2)backlog:表示同一时间最大的并发数。
(4)返回值:成功返回0,失败返回-1。
(5)释:
listen函数声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略。一般backlog的值不会太大,backlog的设置主要是为了提高客户端与服务器的连接效率,太大了会消耗很多内存,得不偿失。
4、connect函数
(1)函数功能:客户端通过调用connect函数来建立和服务器的连接。
(2)函数原型:int connect(int clientfd, const struct sockaddr *serve_addr, socklen_t addrlen)。
(3)参数:
1)clientfd:客户端套接字描述符。
2)serve_addr:服务器套接字地址。
3)addrlen:IPv4结构体的大小,即sizeof(struct sockaddr_in)。
(4)返回值:成功返回0,失败返回-1。
(5)释:
connect函数和bind函数的参数形式一样,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。
5、accept函数
(1)函数功能:服务器通过调用accept函数接收来自客户端的连接请求。
(2)函数原型:int accept(int listenfd, struct sockaddr* cli_addr, int *addrlen)。
(3)参数:
1)sockfd:等待客户端的连接请求侦听描述符。
2)cli_addr:客户端套接字地址。
3)addrlen:套接字地址长度。
(4)返回值:成功返回非负侦听描述符,失败返回-1。
(5)释:
1)三次握手完成后,服务器调用accept函数接受连接。
2)服务器如果调用accept函数时还没有客户端的连接请求,就阻塞等待直到客户端连接上来。
3)addr是一个传出参数,accept函数返回时传出客户端的地址和端口号。
4)如果给addr参数传NULL,则表示不关心客户端的地址。
5)Addrlen参数是一个传入传出参数,传入的是调用者提供的,缓冲区addr的长度以避免缓冲区溢出的问题,传出的是客户端地址结构的实际长度。
四、套接字地址结构
1、sockaddr结构
sockaddr结构是套接字的总体结构,包括16位的地址类型和14字节的地址数据,如下图所示。
在connect、bind和accept函数中要求一个指向与协议相关的的套接字地址结构的指针。如何能接受各种类型的套接字地址结构?解决的办法就是定义套接字函数要求一个指向通用sockaddr结构的指针,然后要求应用程序将与协议特定的结构的指针强制转换成这个通用的指针结构。
2、sockaddr_in结构
IPv4地址用sockaddr_in结构体表示,包括16位的地址类型、16位的端口号和32位的IP地址,其中IPv4的地址用AF_INET表示。sockaddr_in结构体内容如下所示:
struct sockaddr_in {
short int sin_family; /* Address family */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
unsigned char sin_zero[8]; /* Same size as struct sockaddr */
};
(1)sin_family:指代协议族,用AF_INET指代IPv4。
(2)sin_port:端口号,要使用网络字节序(大端存储)。
(3)sin_addr:IP地址,使用in_addr这个数据结构。
struct in_addr {
unsigned long s_addr;
};
释:in_addr用来表示IPv4的IP地址,其实就是一个32位的整数。
(4)sin_zero :是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。
注意:sockaddr_in和sockaddr是并列的结构,指向sockaddr_in的结构体的指针也可以指向sockadd的结构体,并代替它。也就是说,可以使用sockaddr_in建立你所需要的信息,最后再进行类型转换就可以了。
五、数据的转换
1、网络字节序
(1)概念:
网络字序指数据在网络中的存储方式。内存中的多字节数据对于内存地址有大端小端之分,磁盘文件中的多字节数据对于文件偏移量有大端小端之分,网络数据流同样有大端小端之分。TCP/IP协议规定:网络数据流应采用大端存储,即高字节低地址。
(2)字节顺序转换函数
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换,它们存在<arpa/inet.h>头文件中。
1)uint32_t htonl(uint32_t hostlong);
2)uint16_t htons(uint16_t hostshort);
3)uint32_t ntohl(uint32_t netlong);
4)uint16_t ntohs(uint16_t netshort);
释:h表示host,n表示network,l表示32位长整数,s表示16位短整数,to表示转换。即:htonl表示将32位整数由主机字节序转换为网络字节序,返回网络字节序的值;ntohl表示将32位整数由网络字节序转换为主机字节序,返回主机字节序的值。htons和ntohs函数为16位无符号整数执行相应的转换。
2、地址转换函数
网络字节顺序的IP地址是二进制的数据,为了方便使用需要转换为点分十进制的字符串。例如:128.2.194.242就是地址0x8002c2f2的点分十进制表示。应用程序可以使用以下库函数实现IP地址与点分十进制串的转换,它们存放在<arpa/inet.h>头文件中。
(1)int inet_pton(AF_INET, const char *src, void *dst);
释:该函数将一个点分十进制串转换为一个二进制的网络字节顺序的IP地址。如果src没有指向一个合法的点分十进制字符串,那么该函数返回0。成功返回1,失败返回-1。
(2)const char* inet_ntop(AF_INET, const void* src, char* dst,socklen_t size);
释:该函数将一个二进制的网络字节顺序的IP地址转换为它对应的点分十进制的字符串,并把得到的以null结尾的字符串复制到dst。成功返回指向点分十进制的指针,失败返回NULL。
【例1】
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<arpa/inet.h>
int main()
{
struct sockaddr_in addr;
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
uint32_t* ptr = (uint32_t *)(&addr.sin_addr);
socklen_t len = sizeof(struct sockaddr_in);
char arr[1024];
printf("addr:%x\n", *ptr);
printf("addr_str:%s\n", inet_ntop(AF_INET, &addr.sin_addr, arr, len));
return 0;
}
运行结果:
(3)int inet_aton(const char *string, struct in_addr* addr);
释:输入参数string包含ASCII表示的IP地址, 输出参数addr是将要用新的IP地址更新的结构。如果输入地址不正确,则返回0;如果成功,返回非零;如果失败,返回-1。
(4)char *inet_ntoa(struct in_addr in);
释:该函数在内部申请了一块空间保存返回的点分十进制的IP地址。
(5)in_addr_t inet_addr(const char *cp);
释:该函数可将点分十进制的字符串转换为长整型。
六、套接字应用
1、UDP
套接字基于UDP协议的工作过程,如下图所示:
【例2】通过最简单的客户端/服务器程序,实现一个简单的阻塞式网络聊天工具
//服务器
//服务器的作用是与客户端连接,实现数据通信
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main(int argc, char* argv[])
{
if(argc != 3){
printf("Please enter ./serve IP port\n");
return -1;
}
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0){
perror("socket error!\n");
return -1;
}
struct sockaddr_in localaddr;
localaddr.sin_family = AF_INET;
localaddr.sin_port = htons(atoi(argv[2]));
localaddr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t len = sizeof(localaddr);
int ret = bind(sockfd, (struct sockaddr*)&localaddr, len);
if(ret < 0){
perror("bind error!\n");
return -1;
}
char buff[1024];
struct sockaddr_in clientaddr;
while(1){
socklen_t clientlen = sizeof(clientaddr);
ssize_t s = recvfrom(sockfd, buff, sizeof(buff)-1, 0, (struct sockaddr*)&clientaddr, &clientlen);
if(s > 0){
buff[s] = 0;
printf("recv :%s\n", buff);
sendto(sockfd, buff, strlen(buff), 0, (struct sockaddr*)&clientaddr, sizeof(clientaddr));
}
}
close(sockfd);
return 0;
}
//客户端
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
int main(int argc, char* argv[])
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0){
perror("socket error!\n");
return -1;
}
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));
server.sin_addr.s_addr = inet_addr(argv[1]);
char buf[1024];
struct sockaddr_in peer;
while(1){
socklen_t len = sizeof(peer);
printf("Please enter:");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf)-1);
if(s > 0){
buf[s] = 0;
sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&server, sizeof(server));
ssize_t _s = recvfrom(sockfd, buf, sizeof(buf)-1, 0, (struct sockaddr*)&peer, &len);
if(_s > 0){
buf[_s - 1] = 0;
printf("server echo:%s\n", buf);
}
}
}
close(sockfd);
return 0;
}
运行结果:
释:
1)socket的参数使用SOCK_DGRAM表示UDP。
2)bind之后就可以直接进行通信了。
3)UDP使用recvfrom()函数接收数据。recvfrom函数原型为:ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr * src_addr, size_t *addrlen)
,前三个参数等同于函数read()的前三个参数,flags参数是传输控制标志。最后两个参数类似于accept的最后两个参数,接收哪里的数据,及数据大小。
4)UDP使用sendto()函数发送数据。sendto函数原型为:ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr * dest_addr, int addrlen)
,前三个参数等同于函数write()的前三个参数,flags参数是传输控制标志。最后两个参数类似于connect的最后两个参数,发送数据的目的地,及数据大小。
5)atoi是一个把字符串转换为整形数的函数。
2、TCP
套接字基于TCP协议的工作过程如下图所示:
【例3】
//服务器
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define _PORT_ 9999
#define _BACKLOG_ 10
int main()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock < 0){
printf("create socket error, errno is: %d, errstring is: %s\n", errno, strerror(errno));
}
struct sockaddr_in server_socket;
struct sockaddr_in client_socket;
bzero(&server_socket, sizeof(server_socket));
server_socket.sin_family = AF_INET;
server_socket.sin_port = htons(_PORT_);
server_socket.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sock, (struct sockaddr*)&server_socket, sizeof(struct sockaddr_in)) < 0){
printf("bind error, error code is: %d, error string is:%s\n", errno, strerror(errno));
close(sock);
return 1;
}
if(listen(sock, _BACKLOG_) < 0){
printf("listen error, error code is:%d, error string is: %s\n", errno, strerror(errno));
close(sock);
return 2;
}
printf("bind and listen success, wait accept...\n");
for(; ;){
socklen_t len = 0;
int client_sock = accept(sock, (struct sockaddr*)&client_socket, &len);
if(client_sock < 0){
printf("accept error, errno is: %d, error string is; %s\n", errno, strerror(errno));
close(sock);
return 3;
}
char buf_ip[INET_ADDRSTRLEN];
memset(buf_ip, '\0', sizeof(buf_ip));
inet_ntop(AF_INET, &client_socket.sin_addr, buf_ip, sizeof(buf_ip));//存放客户端套接字的地址
printf("get connect, ip is: %s, port is: %d\n", buf_ip, ntohs(client_socket.sin_port));
while(1){
char buf[1024];
memset(buf, '\0', sizeof(buf));
read(client_sock, buf, sizeof(buf));
printf("client say:%s\n", buf);
printf("server say:");
memset(buf, '\0', sizeof(buf));
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf)-1] = '\0';
write(client_sock, buf, strlen(buf)+1);
printf("plase wait...\n");
}
}
close(sock);
return 0;
}
//客户端
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define SERVER_PORT 9999
#define SERVER_IP "192.168.181.129"
int main(int argc, char *argv[])
{
if(argc != 2){
printf("Usage: /client IP\n");
return 1;
}
char *str = argv[1];
char buf[1024];
memset(buf, '\0', sizeof(buf));
struct sockaddr_in server_sock;
int sock = socket(AF_INET, SOCK_STREAM, 0);
bzero(&server_sock, sizeof(server_sock));
server_sock.sin_family = AF_INET;
inet_pton(AF_INET, SERVER_IP, &server_sock.sin_addr);
server_sock.sin_port = htons(SERVER_PORT);
int ret = connect(sock, (struct sockaddr*)&server_sock, sizeof(server_sock));
if(ret < 0){
printf("connect failed..., error code is:%d, error string is:%s\n", errno, strerror(errno));
return 1;
}
printf("connect success...\n");
while(1){
printf("client say:");
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf)-1] = '\0';
write(sock, buf, sizeof(buf));
if(strncasecmp(buf, "quit", 4) == 0){
printf("quit!\n");
break;
}
printf("please wait...\n");
read(sock, buf, sizeof(buf));
printf("server say:%s\n", buf);
}
close(sock);
return 0;
}
(1)测试程序
释:
1)图中可看出server程序的9999端口。
2)netstat命令用来打印Linux中网络系统的状态信息,可让你得知整个Linux系统的网络情况。参数为:
A、-a或–all:显示所有连线中的Socket;
B、-l或–listening:显示监控中的服务器的Socket;
C、-t或–tcp:显示TCP传输协议的连线状况;
D、-n或–numeric:直接使用ip地址,而不通过域名服务器;
E、-p或–programs:显示正在使用Socket的程序识别码和程序名称;
(2)运行结果:
注意:
1)使用fgets函数,如果输入的字符串长度没有超过 n–1,那么系统会将最后输入的回车(换行符 ‘\n’ )保存进来,保存的位置是紧跟输入的字符,然后剩余的空间都用 ‘\0’ 填充。
2)若再启动一个客户端尝试连接服务器,发现第二个客户端无法与服务器连接成功。因为调用accept接受一个请求之后,就在while循环里一直尝试read,没有调用accept函数接受客户端的连接请求,而导致连接失败。
解决方法:
1)通过每个请求,创建子进程的方式来支持多连接,如【例4】。
2)通过每个请求,创建一个线程的方式来支持多连接,如【例5】。
【例4】简单的TCP多进程网络程序
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/wait.h>
void ProcessRequest(int client_fd, struct sockaddr_in* client_addr)
{
char buf[1024];
for( ; ;){
ssize_t read_size = read(client_fd, buf, sizeof(buf));
if(read_size < 0){
perror("read error!\n");
continue;
}
if(read_size == 0){
printf("client: %s say bye!\n", inet_ntoa(client_addr->sin_addr));
close(client_fd);
break;
}
buf[read_size] = '\0';
printf("client %s say: %s\n", inet_ntoa(client_addr->sin_addr), buf);
write(client_fd, buf, strlen(buf));
}
return;
}
void CreateWorker(int client_fd, struct sockaddr_in* client_addr)
{
pid_t pid = fork();
if(pid < 0){
perror("fork error!\n");
return ;
}else if(pid == 0){
//child
if(fork() == 0){
//grand_child
ProcessRequest(client_fd, client_addr);
}
exit(0);
}else{
//father
close(client_fd);
waitpid(pid, NULL, 0);
}
}
int main(int argc, char* argv[])
{
if(argc != 3){
printf("Usage: ./pid_server IP PORT");
return 1;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
addr.sin_addr.s_addr = inet_addr(argv[1]);
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd < 0){
perror("scoket error!\n");
return 1;
}
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0){
perror("bind error!\n");
return 1;
}
ret = listen(fd, 5);
if(ret < 0){
perror("listen error!\n");
return 1;
}
for(; ; ){
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_fd = accept(fd, (struct sockaddr*)&client_addr, &len);
if(client_fd < 0){
perror("accept error!\n");
continue;
}
CreateWorker(client_fd, &client_addr);
}
return 0;
}
问:接收请求时,先由父进程创建子进程,再由子进程创建孙子进程,然后由孙子进程来处理与客户端的交互,为什么?
答:假设不创建孙子进程,由子进程处理与客户端的交互,这时父进程一直在等待子进程的退出而不执行它下面的代码,显然是不行的。创建了孙子进程,由孙子进程处理与客户端的交互,子进程退出,父进程回收子进程,孙子进程被init进程领养。
【例5】简单的TCP多线程网络程序
#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<pthread.h>
typedef struct Arg
{
int fd;
struct sockaddr_in addr;
}Arg;
void *CreateWorker(void* ptr)
{
Arg* arg = (Arg*)ptr;
ProcessRequest(arg->fd, &arg->addr);
free(arg);
return NULL;
}
void ProcessRequest(int client_fd, struct sockaddr_in *client_addr)
{
char buf[1024] = {0};
for( ; ;){
ssize_t read_size = read(client_fd, buf, sizeof(buf));
if(read_size < 0){
perror("read error!\n");
continue;
}
if(read_size == 0){
printf("client: %s say bye!\n", inet_ntoa(client_addr->sin_addr));
close(client_fd);
break;
}
buf[read_size] = '\0';
printf("client:%s say :%s\n", inet_ntoa(client_addr->sin_addr), buf);
write(client_fd, buf, strlen(buf));
}
return;
}
int main(int argc, char *argv[])
{
if(argc != 3){
perror("Usage;./tid_server IP PORT");
return 1;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
addr.sin_addr.s_addr = inet_addr(argv[1]);
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd < 0){
perror("socket error!\n");
return 1;
}
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if(ret < 0){
perror("bind error!\n");
return 1;
}
ret = listen(fd, 5);
if(ret < 0){
perror("listen error!\n");
return 1;
}
for(; ;){
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int client_fd = accept(fd, (struct sockaddr*)&client_addr, &len);
if(client_fd < 0){
perror("accept error!\n");
continue;
}
pthread_t tid = 0;
Arg* arg = (Arg*)malloc(sizeof(Arg));
arg->fd = client_fd;
arg->addr = client_addr;
pthread_create(&tid, NULL, CreateWorker, (void*)arg);
pthread_detach(tid);
}
return 0;
}