socket客户端和服务端代码

Socket是网络协议上的一层抽象接口。本文整理了使用socket实现客户端和服务端的流程。

客户端

1. 使用 socket() 创建TCP套接字

该函数在头文件 sys/socket.h 中:

#include <sys/socket.h>

int socket(int domain, int type, int protocol);

socket函数返回一个文件描述符,代表一个套接字,它是一个整数。按照unix惯例,非负整数表示成功。

第一个参数确定套接字的通信领域,常用的有 AF_INET 表示ipv4,AF_INET6 表示ipv6

第二个参数指定套接字的类型,常用的有 SOCK_STREAM ,它表示可靠的字节流。

第三个参数指定端到端的协议,目前只支持一种协议,也就是TCP/IP协议,这里指定0

因此,可以这样使用:

//创建一个socket
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd < 0)
	error("socket() failed");

2. 使用 connect() 连接服务器

首先,使用套接字需要知道通信的端点的地址,包括ip地址和端口。socket api提供了数据结构用来表示地址信息:

struct sockaddr {
    sa_family_t sa_family;
    char        sa_data[14];
}

这个结构体的第一个成员指定地址族,常用的有 AF_INET 表示ipv4,AF_INET6 表示ipv6 。sockaddr是通用的,而不同的ip版本对应不同的结构

当sa_family指定为 AF_INET 时,就要使用 sockaddr_in 结构

// IPv4 AF_INET sockets:
struct sockaddr_in {
    short            sin_family;       // 2 bytes e.g. AF_INET, AF_INET6
    unsigned short   sin_port;    // 2 bytes e.g. htons(3490)
    struct in_addr   sin_addr;     // 4 bytes see struct in_addr, below
    char             sin_zero[8];     // 8 bytes zero this if you want to
};
struct in_addr {
    unsigned long s_addr;          // 4 bytes load with inet_pton()
};

我们的例子中 服务器地址是 192.168.123.163,端口是8000 ,可以按照下面这样设置sockaddr_in结构体:

//设置要连接的服务端地址,使用结构体addr表示
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
//设置ipv4协议族
addr.sin_family = AF_INET;
//设置ip地址
int ret = inet_pton(AF_INET, "192.168.123.163", &addr.sin_addr.s_addr);
if (ret == 0)
	error("invalid ip address");
else if (ret < 0)
	error("inet_pton() failed");
//设置端口号
addr.sin_port = htons(8000);

设置 sin_family 为 AF_INET ,表示使用ipv4版本

使用 inet_pton 来设置 ip 地址,该函数用于把 点分十进制表示的ip地址转变为 32位的二进制表示

设置端口号8000, 函数 htons 用于 将整型变量转变成网络字节顺序

最后,使用connect()连接服务器

//连接服务端
if (connect(socket_fd, (struct sockaddr *) &addr, sizeof(addr)) < 0)
		error("connect() failed");

connect的第一个参数是套接字描述符,第二个参数就是上面的表示地址的结构体,第三个参数是表示地址结构体的大小。

由于ipv4和ipv6对应了不同的地址结构体,这里传入指针时强制转换为通用的sockaddr指针,根据第一个成员就可以区分ipv4或者是ipv6,从而可以强制转换回去。

3. 使用 send() 和 recv() 通信

//发送字符串hello world
char send_buffer[512] = "hello world";
int len = strlen(send_buffer);
int n = send(socket_fd, send_buffer, len, 0);
if (n < 0)
	error("send() failed");
else if (n != len)
	error("send number of bytes error");
printf("send: %s \n", send_buffer);

//接受服务器返回
char buffer[512];
n = recv(socket_fd, buffer, 511, 0);
if (n < 0)
	error("recv() failed");
buffer[n] = '\0';
printf("receive: %s \n", buffer);

send和recv函数的参数非常类似:

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

对于发送来说,buf指向要发送的数据,len说明要发送的字节数。

对于接收来说,buf指向接收缓冲区,len说明接收区的最大字节数。

如果没有收到消息,recv() 会阻塞。

需要注意的是,recv一次收到的数据不一定是send一次发送的数据,可能需要多次接收。上面的例子中并没有考虑。

4. 使用close()关闭socket

//关闭socket
close(socket_fd);

服务端

1. 使用 socket() 创建TCP套接字

和客户端是类似的

//创建一个socket
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd < 0)
	error("socket() failed");

2. 使用 bind() 给套接字分配端口号

首先也是需要一个表示地址的结构体:

//设置服务端地址,使用结构体server_addr表示
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
//设置ipv4协议族
server_addr.sin_family = AF_INET;
//设置ip地址
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//设置端口号
server_addr.sin_port = htons(8000);

和客户端不同的地方是ip地址,设置为服务端可获得的任何ip地址。htonl 用于 将主机数(32位)转换成无符号长整型的网络字节顺序

接下来使用bind函数,将套接字和本地的一个端口相关联。

//绑定端口
if (bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr)) < 0)
	error("bind() error");

3. 使用 listen() 监听该端口,允许对该端口建立连接

//监听端口
if (listen(socket_fd, 5) < 0)
	error("listen() error");

listen允许客户端连接进入。如果没有执行listen,客户端的connect函数将会失败。

4.1 反复循环,每次使用 accept() 接受连接

需要写到一个循环里,以便反复多次的接收客户端的连接

for (;;)
	{
		struct sockaddr_in client_addr;	//用来保存客户端地址
		int client_addr_len = sizeof(client_addr);

		int client_socket_fd = accept(socket_fd, (struct sockaddr *) &client_addr, &client_addr_len);
		if (client_socket_fd < 0)
			error("accept() error");

		//显示客户端地址
		char client_buffer[512];
		if (inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_buffer, sizeof(client_buffer)) != NULL)
		{
			printf("accept client : %s:%d\n", client_buffer, ntohs(client_addr.sin_port));
		}
		else
			error("client address error");

		//应答客户端
		HandleTCPClient(client_socket_fd);
	}

accept会阻塞直到有客户端连接进来,它返回的是连接到远程连接的套接字。会把客户端的地址放入第二个参数指向的结构,第三个参数存放参数的大小。

使用函数 inet_ntop() 来把 网络字节序的32位二进制ip地址转换为点分十进制的字符串,它正好和函数 inet_pton() 作用相反

4.2 使用 send() 和 recv() 通信 ,使用 close() 关闭

这里的功能是原封不动地将收到的信息发送回去。

void HandleTCPClient(int socket_fd)
{
	//接收客户端消息
	char buffer[512];
	int n = recv(socket_fd, buffer, 511, 0);
	if (n < 0)
		error("recv() failed");
	buffer[n] = '\0';
	printf("receive: %s \n", buffer);

	//发送字符串hello world
	int number_byte_send = send(socket_fd, buffer, n, 0);
	if (number_byte_send < 0)
		error("send() failed");
	else if (number_byte_send != n)
		error("send number of bytes error");
	printf("send: %s \n", buffer);


	//关闭socket
	close(socket_fd);
}

 

如果文章对你有帮助,欢迎点赞或打赏(金额不限)。你的打赏将全部用于支付网站服务器费用和提高网站文章质量,谢谢支持。

版权声明:

本文由 原创,商业转载请联系作者获得授权。
非商业转载请注明作者 雅乐网 ,并附带本文链接:
https://www.yalewoo.com/socket_linux.html

上一篇:

下一篇:

文章《socket客户端和服务端代码》共有1条评论:

  1. 毫无疑问,这个是要支持的!

我要评论

验证码*: 6 + 4 =