Linux下的C Socket编程 -- 简介与client端的处理

时间:2024-02-22 18:19:29

Linux下的C Socket编程(一)

介绍

Socket是进程间通信的方式之一,是进程间的通信。这里说的进程并不一定是在同一台机器上也有可能是通过网络连接的不同机器上。只要他们之间建立起了socket的连接,那么数据便可以在机器之间进行双向的交流,直到连接断开。

socket的建立

在我们接触到实际的代码API之前,我们应该对基础的连接方式有所了解。

Note left of server: 建立一个正被监听的socket并等待客户端的连接
Note right of client: 建立一个客户端的socket并尝试连接server
Note left of server: 接受来自client的连接请求
server->client: 发送与接受数据
client->server: 接受与发送数据
Note left of server: 关闭当前的连接
Note right of client: 关闭当前的连接

上图便是基础的连接方式。

  1. 首先server需要创建正在被监听socket,等待client的连接请求。
  2. client创建一个socket,尝试连接server
  3. server接受client的请求,建立起两者之间的连接。
  4. 数据交换,双向通信
  5. 任何一方都可以断开连接,断开后连接会自动销毁。

根据相应的流程,其对应的客户端与服务端的流程也不尽相同:

对于客户端来说为:

  1. 通过系统函数socket()创建一个socket
  2. 通过系统函数connect()server端的socket发起请求。
  3. 交换数据,实现这种数据交换的方式有很多种方式,其中最简单的就是使用系统函数read(),write()

对于服务端来说:

  1. 通过系统函数socket()创建一个socket。
  2. 通过系统函数bind()绑定这个socket到server的一个端口上。
  3. 通过系统函数listen()监听这个socket。
  4. 当监听到又一个请求来临时,通过系统函数accept()接受一个请求。这个函数会阻塞io直到两者的连接完全断开。
  5. 交换数据。

socket的类型

当一个socket被建立起来之后,进程间需要去说明所使用的协议socket type。只有通信的双方都拥有相同的type协议

目前广泛使用的协议有两大类,分别是Unix文件系统协议(Unix domain)Internet网络协议(Internet domain)。对应的他们有各自的特点。使用Unix domain的双方使用公共的文件系统进行通信,使用Internet domain的进程分别位于不同的主机上,他们通过网络进行通信。

使用Unix domain的socket地址本质上就是文件系统的一个记录,本身是一条字符串。
使用Interner domain的socket包含两部分,一部分是主机的IP地址,一部分是socket绑定到的端口号。一般端口号比较低的端口都会被当作特殊的用途,比如端口号是80的端口是提供http服务的。

目前广泛使用的socket类型也是两种,一种是流socket(stream sockets),一种是数据报socket(datagram sockets)stream socket处理通信就像是处理流水一样的连续不断的字节流,而datagram sockets需要读取完整的字符,通常一个字符由几个字节组成。

接下来的内容是建立在使用TCP协议的基础上,这是一种可靠的面向字节流的协议,另外一种协议是UDP协议,这是一种不可靠的面向字符的协议。

client端的简单的示例

创建socket

不管是server还是client,第一步都是创建socket:

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>

int main(int argc, char *argv[]) {
	int socket_desc;
	
	socket_desc = socket(AF_INET, SOCK_STREAM, 0);
	
	if (-1 == socket_desc) {
		perror("cannot create socket");
		exit(1);
	}
}

socket()函数创建一个socket并且返回一个对应的描述符。
其参数分别为:

  1. Address Family - AF_INET => IPv4
  2. Type - SOCK_STREAM => socket连接使用TCP协议
  3. Protocol - 0 => 使用IP协议
    更多的可以参看socket()的用法。
发起连接

我们通过IP地址和端口号去连接远程主机,为此我们需要创建正确的结构体去保存远程主机的基本信息,从而表示远程主机。

struct sockaddr_in server;

socketaddr_in是一个包含网络地址的结构体,下面是他的定义:

struct sockaddr_in {
    short            sin_family;   // IP协议族,e.g. AF_INET, AF_INET6
    unsigned short   sin_port;     // 远程端口号,e.g. htons(3490)
    struct in_addr   sin_addr;     // 参见下面的in_addr结构体
    char             sin_zero[8];  // zero this if you want to
};

struct in_addr {
    unsigned long s_addr;
};

可以看到这个结构体内部还有一种类型为in_addr,其内部的结构知识一个long类型的数据。IP地址便保存在这个long的类型中。

函数inet_addr()可以很方便的将IP地址转换为long类型的格式。

server.sin_addr.s_addr = inet_addr("127.0.0.1");

既然知道了远程主机server的地址,那么接下来便是发起连接了:

#include<stdio.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<sys/types.h>
// struct sockaddr_in
#include<netinet/in.h>
// inet_addr
#include<arpa/inet.h>

int main() {
	int socket_desc;
	struct sockaddr_in server;
	
	// 创建socket
	socket_desc = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == socket_desc) {
		perror("cannot create socket");
		exit(1);
	}
	
	// 设置远程服务器的信息
	server.sin_addr.s_addr = inet_addr("127.0.0.1");
	server.sin_family = AF_INET;
	server.sin_port = htons(80);
	
	// 连接
	if (connect(socket_desc, (struct sockaddr *)&server, sizeof(server)) < 0) {
		perror("cannot connect");
		return 0;
	}
	
	// 当服务器接受请求时便会建立连接
	printf("connect success");
	return 0;
}

connect()函数会向服务器发起请求建立一个连接。
其参数为:

  1. int sockfd => socket的描述符
  2. const struct sockaddr *addr => sockaddr的结构体,通用的socket地址
  3. socklen_taddrlen => socket描述符的长度。

struct sockaddr是通用的套接字地址,而struct sockaddr_in则是Internet环境下套接字的地址形式,二者长度一样,都是16个字节。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。一般情况下,需要把sockaddr_in结构强制转换成sockaddr结构再传入系统调用函数中。更多

参见connect()

另外代码中的htons()函数的作用是将主机的数据转化为网络字节的顺序,更多的的可以参见这里
至于为什么要转换数据的字节顺序,这里就不说啦,可以自己去找找历史看看哇~

到了这里,我们不仅创建了socket而且也已经成功的连接了服务器。下面便是向服务器进行通信了。

在socket之上发送数据

函数send()实现发送数据的功能:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>

int main() {
	int socket_desc;
	struct sockaddr_in server;
	char *message;
	
	socket_desc = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == socket_desc) {
		perror("cannot create socket");
		exit(1);
	}
	
	server.sin_addr.s_addr = inet_addr("127.0.0.1");
	server.sin_family = AF_INET;
	server.sin_port = htons(80);
	
	if (connect(socket_desc, (struct sockaddr *)&server, sizeof(server)) < 0) {
		perror("connect error");
		return 1;
	}
	
	printf("connect success\n");
	message = "hello world";
	if (send(socket_desc, message, strlen(message), 0) < 0) {
		printf("send error");
		return 2;
	}
	
	printf("message send success");
	return 0;
}

send()函数实现的是向服务器发送数据,它其实就是向socket写数据,类似的就像是向文件中写入数据。

其参数为:

  1. int sockfd => 指定发送数据的socket描述符
  2. const void *buff => 发送数据
  3. size_t nbytes => 发送数据的长度
  4. int flags => 标志

更过的可以参见send()linux send与recv函数详解

到现在为止,我们便完成了client端的大部分操作,已经能够向对方发送数据,那么接下来就是接收服务端返回的数据了。

通过socket接收数据

函数recv()用来接收socket的数据:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>

int main() {
	int socket_desc;
	struct sockaddr_in server;
	char *message, server_reply[2000];
	
	// 创建socket
	socket_desc = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == socket_desc) {
		perror("connot create socket");
		exit(1);
	}
	
	server.sin_addr.s_addr = inet_addr("127.0.0.1");
	server.sin_family = AF_INET;
	server.sin_port = htons(80);
	
	// 进行连接
	if (connect(socket_desc, (struct sockaddr*)&server, sizeof(server)) < 0) {
		perror("connot connect");
		return 1;
	}
	
	// 发送数据
	message = "GET / HTTP/1.1\r\n\r\n";
	if (send(socket_desc, message, strlen(message), 0) < 0) {
		perror("send data error");
		return 2;
	}
	
	printf("send message success\n");
	// 接收数据
	if (recv(socket_desc, server_reply, 2000, 0) < 0) {
		perror("recv error");
		return 3;
	}
	
	printf("recv success");
	puts(server_reply);
	
	return 0;
}

recv()函数就是为了接收socket的数据,其参数为:

  1. int sockfd => 接收端的socket描述符
  2. void *buff => 存放数据的缓冲区,数据存放在*buff
  3. size_t nbytes => 指明buff的长度
  4. int flags => 一般设置为0

更过的可以参见:recv()linux send与recv函数详解

至此,我们完成了socket通信的主要流程,成功的拿到了服务端返回的数据,在上方通信完成之后便可以将此socket连接关闭。

关闭socket

关闭socket很简单,只需要在最后加上一句:

// 需要包含此头文件
#include<unistd.h>

close(socket_desc);

便可完成关闭。

client端的总结:

所以经过上面的讨论,最终的全部代码为:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<unistd.h>

int main() {
	int socket_desc;
	struct sockaddr_in server;
	char *message, server_reply[2000];
	
	// 创建socket
	socket_desc = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == socket_desc) {
		perror("connot create socket");
		exit(1);
	}
	
	server.sin_addr.s_addr = inet_addr("127.0.0.1");
	server.sin_family = AF_INET;
	server.sin_port = htons(80);
	
	// 进行连接
	if (connect(socket_desc, (struct sockaddr*)&server, sizeof(server)) < 0) {
		perror("connot connect");
		return 1;
	}
	
	// 发送数据
	message = "GET / HTTP/1.1\r\n\r\n";
	if (send(socket_desc, message, strlen(message), 0) < 0) {
		perror("send data error");
		return 2;
	}
	
	printf("send message success\n");
	// 接收数据
	if (recv(socket_desc, server_reply, 2000, 0) < 0) {
		perror("recv error");
		return 3;
	}
	
	printf("recv success");
	puts(server_reply);
	
	// 关闭socket
	close(socket_desc);
	return 0;
}