Linux应用编程—13.网络编程
网络编程要熟悉一些计算机网络有关的名词,可以先做了解,后续实际开发在深入研究,自上而下学习。与本次Linux应用编程有关的就是TCP与UDP协议。简明概念如下图1所示。
前面学习的进程间通讯的方法有:管道、共享内存、消息队列。进程间同步的方法有:信号量、互斥锁。socket也是一种进程间进行数据交互的方法,用于网络连接的不同主机上的应用层程序进行数据交互。
13.1 服务器与客户端数据交互过程
socket()函数系统调用创建一个新的socket(),使用时需要包含头文件sys/socket.h,函数原型为:int socket(int domain, int type, int protocol);函数成功时返回一个新建的socket的文件描述符。参数1domain:有3个可选项,确定了socket的通讯范围,如同一个主机上的应用程序之间还是位于使用一个网络连接起来的不同主机上的应用程序之间。一般可选参数如下:
Domain | 执行的通讯 | 应用程序之间的通讯 | 地址格式 | 地址结构 |
---|---|---|---|---|
AF_UNIX | 内核中 | 同一主机 | 路径名 | sockaddr_un |
AF_INET | IPv4 | 通过IPv4连接起来的主机 | 32位IPv4地址 + 16位端口号 | sockaddr_in |
AF_INET6 | IPv6 | 通过IPv6连接起来的主机 | 128位IPv6地址 + 16位端口号 | sockaddr_in6 |
参数2 type:socket有两种,流和数据报。它们区别如下表2所示:
属性 | socket类型 | |
---|---|---|
流 | 数据报 | |
可靠地递送? | 是 | 否 |
消息边界保留? | 否 | 是 |
面向连接? | 是 | 否 |
其实根据这里地特点,可以联系到TCP与UDP的特点。其实,数据报socket使用了UDP协议,流socket使用了TCP协议。专业术语“UDP socket”和“TCP socket”。
参数3protocol:一般使用默认值0即可。
函数bind()作用是将一个socket绑定到一个地址上,函数原型位:int bind(int sockfd, const struct *addr, socklen_t addrlen);调用需要包含头文件sys/socket.h。参数1:sockfd是上一个socket()返回的文件描述符;参数0:addr是一个指针,指向了一个指定该socket绑定到的地址的结构,传入这个参数的结构的类型取决于socket domain。参数3:addelen参数指定了地址结构的大小。
根据上面的domain的3种类型,地址格式各不相同。保存这个地址需要定义不同的结构体才行,但为了这个结构体类型能够接受任意数据的地址结构,socket API定义了一个通用的地址结构struct skcketaddr。用来将不同的domain特定的地址结构转换为单个类型供socket系统调用的各个参数使用。socketaddr结构体通常定义如下所示:
struct sockaddr
{
sa_family_t sa_family; // AF_ * constant, sa_family_t 整数类型
char sa_data[14]; // socket addr, size varies according to socket domain.
};
实际使用
函数listen()将文件描述符sockfd引用的流socket标记为被动,这个socket后面会被用来接受来自其它(主动的)socket的连接。函数原型为int listen(int sockfd, int backlog);使用时需要包含头文件sys/socket.h。参数1:sockfd是上一个socket()返回的文件描述符;参数2:backlog表示允许限制这种未决连接的数量,在这个数量之内连接请求会立即成功。什么是未决连接,如下图3所示:
函数accept()在文件描述符sockfd引用的监听流socket上接受一个接入连接。如果在调用accept()时不存在未决的连接,那么调用就会阻塞直到有连接请求为止。函数原型为int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);调用时需要包含头文件sys/socket.h。函数accept()调用返回结果是已连接的socket的文件描述符。函数accept()会创建一个新的socket,这个新的socket会与执行connect()的对等socket()进行连接。addr参数指向一个用来返回socket地址的结构,该参数类型取决于socket domain。参数addrlen指向一个整数,调用前需要将这个整数初始化为addr指向的缓冲区的大小。内核才知道有多少空间用于返回socket地址。当accept()返回后,这个addrlen会被设置成实际被复制进缓冲区中的数据的字节数。
函数connect()用于连接到对等socket,将文件描述符sockfd引用的主动socket连接到地址通过addr和addrlen指定的监听socket上。函数原型为int connect(int sockfd, const struct sockaddr * addr, socklen_t addrlen);调用需要包含头文件sys/socket.h。参数1:文件描述符sockfd是主动socket将它连接到地址通过addr和addrlen指定的监听socket上。剩余参数addr与addrlen参数指定方式与bind()函数相同。
函数close()用来终止一个流socket。
13.2 编写一个TCP服务器
服务器代码:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define FAILE -1
#define SUCCESS 0
#define PORT 8800
#define SIZE 100
int main(void)
{
int ret = 0;
int sock_fd, client_sockfd = 0;
int addr_len = sizeof(struct sockaddr);
char str[SIZE] = "Welcom to connect Server.";
struct sockaddr_in my_sockaddr, client_addr;
my_sockaddr.sin_family = AF_INET;
my_sockaddr.sin_port = htons(PORT); // htons()将整数转为网络字节序
my_sockaddr.sin_addr.s_addr = INADDR_ANY;
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if(sock_fd == FAILE)
perror("socket.");
ret = bind(sock_fd, (struct sockaddr *)&my_sockaddr, sizeof(struct sockaddr));
if(ret == FAILE)
perror("bind.");
ret = listen(sock_fd, 10);
if(ret == FAILE)
perror("listen.");
while(1)
{
printf("Server is waiting for client to connect:\n");
client_sockfd = accept(sock_fd, (struct sockaddr *)&client_addr, &addr_len);
printf("Client address = %s\n", inet_ntoa(client_addr.sin_addr)); // inet_ntoa()将网络地址转换成“.”点隔的字符串格式
send(client_sockfd, str, SIZE, 0);
printf("Disconnect the client request.\n");
close(client_sockfd); // 关闭主动
}
close(sock_fd);
return 0;
}
指定编译为SERVER可执行性文件。
运行结果:
打印字符串,代码阻塞到accept()函数,等待有客户端连接。
客户端运行时,通过main()函数传参传入IP地址。然后连接服务器,连接到后,会收到服务器发送的字符串,”Welcom to connect Server.“。打印字符串,并且关闭sock_fd。
客户端代码:
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define FAILE -1
#define SUCCESS 0
#define PORT 8800
#define SIZE 100
int main(int argc, char * argv[])
{
int ret = 0;
int sock_fd = 0;
struct sockaddr_in server_addr;
char buff[SIZE];
if(argc < 2)
{
printf("Usage: ./client [server IP address.\n]");
}
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if(sock_fd == FAILE)
perror("socket.");
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = inet_addr(argv[1]);
ret = connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr));
if(ret == FAILE)
perror("connext.");
recv(sock_fd, buff, SIZE, 0);
printf("Client receive from server: %s\n", buff);
close(sock_fd);
return 0;
}
指定编译为CLIENT可执行性文件。
运行代码,./CLIENT 192.168.20.129,这里的ip是虚拟机的ip地址,通过ifconfig查询。
运行结果: