文章目录
前言
学习 Linux Socket 编程,总结一些心得
一、什么是Socket?
大部分的网络应用系统可以分为两个部分:客户端(Client)和服务器端(Server),而网络服务程序有两种模式
- ①、CS模式
- ②、BS模式
其中我们最常用的就是 C/S 模式,Socket通信也是属于这一类别的。
在计算机网络中,我们可以知道网络层的 “IP地址”可以唯一标识网络中的主机,而传输层的 “端口” 可以唯一标识主机中的应用程序(进程)。利用这两者即可标识网络中的进程,所以,网络中的进程通信就可以利用这两个标志与其他进程进行交互。
使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是为什么说“一切皆socket”。 TCP/IP协议族包括运输层、网络层、链路层,而socket所在位置如图,Socket是应用层与TCP/IP协议族通信的中间软件抽象层。
只要是类UNIX系统的,通信时采用的都是socket通信。
由上图可知,Socket 是建立在用户进程和TCP之间的,我们在编写Socket程序的时候,是不需要去管TCP/IP的实现的,操作系统会自动封装TCP/IP的首部,我们只需要调用Socket的相应接口函数即可。
Socket 起源于 Unix ,而 Unix / Linux 的基本哲学之一就是 “ 一切皆文件 ”。在许多操作系统中,套接字API最初是作为UNIX操作系统的一部分而开发的,所以套接字API与系统的其他I/O设备集成在一起。应用程序要为因特网通信而创建一个套接字(socket)时,操作系统就返回一个小整数作为描述符(descriptor)来标识这个套接字。然后应用程序以该描述符作为传递参数,通过调用相应函数(如read、write、close等)来完成某种操作(如从套接字中读取或写入数据)。
在生活中,A要电话给B,A拨号,B听到电话铃声后提起电话,这时A和B就建立起了连接,A和B就可以讲话了。等交流结束,挂断电话结束此次交谈。 打电话很简单解释了这工作原理:“open—write/read—close”模式。下面是网络socket通信的基本流程:
Socket通信的过程中,大概的流程如下
服务器端:
- socket会创建一个文件描述符fd,负责与客户端连接
- 然后bind(),就是把文件描述符和端口绑定到一起
- 然后服务器就可以开始监听
- 接着服务器就转为accept(),阻塞着(一直在等着客户端来连接)
- 连上后,创建一个新的client_fd,使用新的 client_fd 和客户端进行通信
- 用完之后就close()
客户端:
- socket会创建一个文件描述符fd
- 然后connect(),传IP和端口进去,连接服务器
- 建立连接后,就可以与服务器进行通信(读/写)
- 用完之后就close()
二、函数分析
1、socket()函数
int socket(int domain, int type, int protocol);
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket
descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:
- domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
- type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的类型有哪些?)。
- protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、PPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议,type和protocol并不是可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
2、bind() 函数
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数的三个参数分别为:
- sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
- addrlen:对应的是地址的长度。
- addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,但最终都会强制转换后赋值给sockaddr这种类型的指针传给内核:
通用套接字 sockaddr 类型定义:
typedef unsigned short int sa_family_t;
struct sockaddr {
sa_family_t sa_family; /* 2 bytes 地址族, AF_xxx */
char sa_data[14]; /* 14 bytes 协议地址 */
}
ipv4对应的是sockaddr_in类型定义:
typedef unsigned short sa_family_t;
typedef uint16_t in_port_t;
struct in_addr {
uint32_t s_addr;
};
struct sockaddr_in {
sa_family_t sin_family; /* 2 bytes 地址族, AF_xxx 比如IPv4为 AF_INET */
in_port_t sin_port; /* 2 bytes 端口*/
struct in_addr sin_addr; /* 4 bytes IPv4 地址*/
/* 填充至 `struct sockaddr' 的长度 */
unsigned char sin_zero[8]; /* 8字节未使用的填充数据,总是设置为零 */
};
由于本次程序编写只涉及到IPv4,则先暂时介绍IPv4
3、listen() 函数
socket()函数创建的socket默认是一个主动类型的,如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,该函数将socket变为被动类型的,等待客户的连接请求。
int listen(int sockfd, int backlog);
- sockefd: socket()系统调用创建的要监听的socket描述字
- backlog: 相应socket可以在内核里排队的最大连接个数
4、connect() 函数
TCP 客户端程序调用 socket() 创建 socket fd 之后,就可以调用 connect()
函数来连接服务器。如果客户端这时调用 connect() 发出连接请求,服务器端就会接收到这个请求并使 accept()
返回,accept() 返回的新的文件描述符就是对应到该客户的 TCP 连接,通过这两个文件描述符(客户端 connect 的 fd
和服务器端 accept 返回的 fd )就可以实现客户端和服务器端的相互通信。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd: 客户端的socket()创建的描述字
- addr: 要连接的服务器的socket地址信息,这里面包含有服务器的IP地址和端口等信息
- addrlen: socket地址的长度
5、accept() 函数
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。服务器之后就会调用accpet()接受来自客户端的连接请求,这个函数默认是一个阻塞函数,这也意味着如果没有客户端连接服务器的话该程序将一直阻塞着不会返回,直到有一个客户端连过来为止。一旦客户端调用connect()函数就会触发服务器的accept()返回,这时整个TCP链接就建立好了。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- sockfd: 服务器开始调用socket()函数生成的,称为监听socket描述字;
- *addr: 用于返回客户端的协议地址,这个地址里包含有客户端的IP和端口信息等;
- addrlen: 返回客户端协议地址的长度
三、具体代码
1、注意事项
- ①、服务器端程序需要带端口参数运行,客户端程序执行时需要带服务器IP、端口作为命令行参数,不然无法正常运行,比如:
./server 12345
./client 127.0.0.1 12345
- ②、服务器端程序要先运行,如果客户端程序先运行的话,会因为找不到服务器程序而出现 “ Connection refused ”错误
- ③、由于本次代码没有使用 多进程/多线程/多路复用 的功能,程序运行的时候,只能同时与一个客户端保持通信
- ④、若在通信过程中,客户端主动断开连接,服务器端则会断开与该客户端的连接,并且等待下一个客户端接入。
- ⑤、若在通信过程中,服务器端程序终止运行,则客户端程序会因为 read() 返回值为0,判断连接断开,自动终止客户端程序。
2、服务器端源码
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdlib.h>
#define MSG_STR "Hello, Simply!"
int main(int argc, char **argv)
{
int listen_fd = -1;
int client_fd = -1;
int on = 1;
struct sockaddr_in servaddr;
struct sockaddr_in cliaddr;
socklen_t cliaddr_len;
int server_port;
int backlog = 10;
int rv = -1;
char buf[1024];
//用来确认程序执行的格式是否正确,不正确则退出并提醒用户
if (argc < 2)
{
printf("Program usage: %s [Port]\n", argv[0]);
return -1;
}
//将端口参数赋给参数变量
//由于命令行传参进来是字符串类型,所以需要atoi转换为整型
server_port = atoi(argv[1]);
/*
* socket(),创建一个新的sockfd
* 指定协议族为IPv4
* socket类型为SOCK_STREAM(TCP)
*/
listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0)
{
printf("create socket failure: %s\n", strerror(errno));
return -2;
}
printf("create socket[%d] success\n", listen_fd);
//避免上次结束程序时,端口未被及时释放的问题
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
/*
* bind(),将服务器的协议地址绑定到listen_fd
*/
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(server_port);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
{
printf("socket[%d] bind port[%d] failure: %s\n", listen_fd, server_port, strerror(errno));
close(listen_fd);
return -3;
}
printf("socket[%d] bind port[%d] success\n", listen_fd, server_port);
/*
* listen()
* 监听listen_fd的端口,并设置最大排队连接个数
*/
listen(listen_fd, backlog);
printf("Start listening port[%d]\n", server_port);
/*
* accept()
* 等待并接受来自客户端的连接请求
* 如果没有客户端连接服务器的话该程序将一直阻塞着不会返回,直到有一个客户端连过来为止
* 返回一个client_fd与客户通信
*/
while (1)
{
printf("\nStart waitting and accept new client to connect...\n");
client_fd = accept(listen_fd, (struct sockaddr *)&cliaddr, &cliaddr_len);
if (client_fd < 0)
{
printf("accept new client failure: %s\n", strerror(errno));
continue;
}
printf("accept new client [%s:%d] with fd[%d] success\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port), client_fd);
//与客户端保持通信
while (1)
{
//清空buf内容,避免因为随机值出现乱码
memset(buf, 0, sizeof(buf));
//从客户端中读取数据
rv = read(client_fd, buf, sizeof(buf));
if (rv < 0)
{
printf("socket[%d] read date from client failure: %s\n", client_fd, strerror(errno));
break;
}
else if (rv == 0)
{
printf("socket[%d] get Disconnected\n", client_fd);
break;
}
else if (rv > 0)
{
printf("socket[%d] read %d Byte data from client: %s\n", client_fd, rv, buf);
}
//发送消息给客户端
rv = write(client_fd, MSG_STR, strlen(MSG_STR));
if (rv < 0)
{
printf("socket[%d] write date to client failure: %s\n", client_fd, strerror(errno));
break;
}
printf("socket[%d] write %d Byte data to client: %s\n", client_fd, rv, MSG_STR);
printf("\n");
}
printf("close client[%d]\n", client_fd);
close(client_fd); //关闭与客户端的连接
}
/*
* close()
* 关闭服务器监听的listen_fd
*/
printf("Close socket[%d]\n", listen_fd);
close(listen_fd);
}
3、客户端源码
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdlib.h>
#define MSG_STR "Hello, Unix Network Program World!"
int main(int argc, char **argv)
{
int conn_fd = -1;
struct sockaddr_in servaddr;
char *server_ip;
int server_port;
int rv = -1;
char buf[1024];
//用来确认程序执行的格式是否正确,不正确则退出并提醒用户
if (argc < 3)
{
printf("Program usage: %s [IPaddr] [Port]\n", argv[0]);
return -1;
}
//将命令行参数传给IP地址变量
//将端口参数赋给参数变量,由于命令行传参进来是字符串类型,所以需要atoi转换为整型
server_ip = argv[1];
server_port = atoi(argv[2]);
/*
* socket(),创建一个新的sockfd
* 指定协议族为IPv4
* socket类型为SOCK_STREAM(TCP)
*/
conn_fd = socket(AF_INET, SOCK_STREAM, 0);
if (conn_fd < 0)
{
printf("create connect socket failure: %s\n", strerror(errno));
return -1;
}
printf("create connet socket[%d] success\n", conn_fd);
/*
* connect()
* 连接服务器
*/
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(server_port);
inet_aton(server_ip, &servaddr.sin_addr);
if (connect(conn_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
{
printf("socket[%d] connect server[%s:%d] failure: %s\n", conn_fd, server_ip, server_port, strerror(errno));
close(conn_fd);
return -2;
}
printf("socket[%d] connect server[%s:%d] success\n", conn_fd, server_ip, server_port);
/*
* r/w
* 读写操作
* 向服务器发送字符串消息:"Hello, Unix Network Program World!"
* 读取服务器发送的的消息
*/
while (1)
{
rv = write(conn_fd, MSG_STR, strlen(MSG_STR));
if (rv < 0)
{
printf("socket[%d] write date to server failure: %s\n", conn_fd, strerror(errno));
break;
}
printf("socket[%d] write %d Byte data to server: %s\n", conn_fd, rv, MSG_STR);
memset(buf, 0, sizeof(buf));
rv = read(conn_fd, buf, sizeof(buf));
if (rv < 0)
{
printf("read date from server failure: %s\n", strerror(errno));
break;
}
else if (rv == 0)
{
printf("socket[%d] get Disconnected\n", conn_fd);
break;
}
else if (rv > 0)
{
printf("socket[%d] read %d Byte data from server: %s\n\n",conn_fd, rv, buf);
}
sleep(1);
}
/*
* close()
* 关闭与服务器进行通信的conn_fd
*/
printf("Close socket[%d]\n", conn_fd);
close(conn_fd);
}
四、实现效果
总结
以上是对Linux Socket编程的一些理解,如有写的不好的地方,还请各位大佬不吝赐教