C 网络与套接字socket

来源(博客园)

From: https://www.cnblogs.com/machao/p/5654661.html

C 网络与套接字socket

我们已经知道如何使用I/O与文件通信,还知道了如何让同一计算机上的两个进程进行通信,这篇文章将创建具有服务器和客户端功能的程序

互联网中大部分的底层网络代码都是用C语言写的。 网络程序通常有两部分组成:服务器和客户端。

工具介绍: telnet

为了测试功能,我们使用一个叫做telnet的客户端程序连接服务器,telnet 接受两个参数:一个是服务器地址,另一个是服务器运行的端口号,

如果在运行服务器的那台计算机上运行telnet,地址可填写127.0.0.1

这样使用:假设端口号是30000

telnet 127.0.0.1 30000



我们先说服务器这一端的:

服务器连接网络分为四部曲:①绑定(Bind) ②监听(Listen) ③接受(Accept) ④开始(Begin)

把每个首字母连起来就是BLAM

如果想写一个与网络通信的程序,就需要一种新的数据流---套接字

#include <sys/socket.h>
int listener_d = socket(PF_INET, SOCK_STREAM, 0);
if (listener_d == -1) {
	error("不能打开套接字");
}


其中 listener_d 是套接字描述符 / 0 是协议号,一般填0就行

1.绑定(Bind)

计算机可能同时运行多个服务器程序,一个发送网页,一个发送邮件,另一个运行聊天服务器。为了防止不同对话发生混淆,每项服务必须使用不同的端口(port)。

端口就好比电视频道,我们在不同的端口使用不同的网络服务,就像我们在不同频道收看不同的电视节目。

#include <arpa/inet.h>

// 绑定端口
struct sockaddr_in name;
name.sin_family = PF_INET;
name.sin_port = (in_port_t)htons(30000);
name.sin_addr.s_addr = htonl(INADDR_ANY);
int c = bind(listener_d, (struct sockaddr *)&name, sizeof(name));
if (c == -1) {
	error("无法绑定端口");
}


2.监听(Listen)

通常会有很多客户端连接到服务器,如果我们想要客户端排队等待连接,就要使用listen()来告诉操作系统你希望队列有多长。

// 监听
if (listen(listener_d, 10) == -1) {
	error("无法监听");
}


调用listen()把队列长度设为10,也就是说最多可以有10个客户端可以尝试连接服务器,他们并不会立刻得到相应,但是可以排队等待,而第11个客户端会被告知服务器太忙了。



3.接受连接(Accept)

对于服务器端来说,当我们已经绑定完了端口,设置了监听队列,唯一可做的就是等待了。服务器一生都在等待客户端来连接他们,accept()调用会一直等待,知道有客户端链接服务器时,他会返回第二个套接字描述符,然后就可以通信了。

// 接受链接
struct sockaddr_storage client_addr; // 保存链接客户端的相信信息
unsigned int address_size = sizeof(client_addr);
int connect_d = accept(listener_d, (struct sockaddr *)&client_addr, &address_size);
if (connect_d == -1) {
	error("无法打开副套接字");
}


套接字并不是传统意义上的数据流

我们知道的数据流有:文件,标准输入,标准输出。都可以使用fprintf和fscanf函数和他们通信,这俩个函数都是单向的,但套接字不同,套接字是双向的,既可以用作输出,也可以用作输入,因此需要别的函数。

输出:send() 输入:recv()

我们先介绍send函数

char *msg = "Internet Knock-Knock Protocol Server\r\nVersion 1.0\r\nKnock! Knock!\r\n>";

if (send(connect_d, msg, strlen(msg), 0) == -1) {
	error("send");
}




好了,让我们先用一个例子来演示一下上边的功能怎么用,先看代码:

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>

void error(char *msg) {
	fprintf(stderr, "Error: %s  %s", msg, strerror(errno));
	exit(1);
}

int main(int argc, const char * argv[]) {

	char *advice[] = {
		"你为什么这么帅!\r\n",
		"有没有人夸过你帅?",
		"傻逼牛头,笨鳖",
		"牛,你是第六人吗?",
		"拔插座了吧"};

	// 打开
	int listener_d = socket(PF_INET, SOCK_STREAM, 0);
	if (listener_d == -1) {
		error("不能打开套接字");
	}

	// int reuse = 1;
	// if (setsockopt(listener_d, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(int)) == -1) {
	//    	error("无法设置套接字的“重新使用端口”选项");
	// }
	// 绑定端口
	struct sockaddr_in name;
	name.sin_family = PF_INET;
	name.sin_port = (in_port_t)htons(30000);
	name.sin_addr.s_addr = htonl(INADDR_ANY);
	int c = bind(listener_d, (struct sockaddr *)&name, sizeof(name));
	if (c == -1) {
		error("无法绑定端口");
	}

	// 监听
	if (listen(listener_d, 10) == -1) {
		error("无法监听");
	}

	puts("等待链接...");

	while (1) {
		// 接受链接
		struct sockaddr_storage client_addr; // 保存链接客户端的相信信息
		unsigned int address_size = sizeof(client_addr);
		int connect_d = accept(listener_d, (struct sockaddr *)&client_addr, &address_size);
		if (connect_d == -1) {
			error("无法打开副套接字");
		}

		// 通信
		//    char *msg = "Internet Knock-Knock Protocol Server\r\nVersion 1.0\r\nKnock! Knock!\r\n>";
		char *msg = advice[rand() % 5];
		if (send(connect_d, msg, strlen(msg), 0) == -1) {
			error("send");
		}

		if (close(connect_d) == -1) {
			error("无法关闭链接");
		}

	}

	return 0;
}


// Mac 下编译运行

gcc socket.c -o socket

./socket

终端显示成这样



我们打开另一个终端来模拟客户端



太棒了,服务器和客户端能够连接且服务器能够给客户端发送数据了,但是这样的程序还是有问题的,当我们快速使用Ctrl-C结束服务器的程序,在用./socket打开机会出现这样的错误



为什么会出现这个错误呢?因为绑定端口是有延时的。

当你在某个端口绑定了一个程序,系统不允许在30秒内再绑定其他的程序,也包括上一次绑定这个端口的程序。只要在绑定前设置套接字的某个选项就能解决这个问题

把上边的代码注释的地方打开

int reuse = 1;
if (setsockopt(listener_d, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(int)) == -1) {
	error("无法设置套接字的“重新使用端口”选项");
}


重复之前的的操作,Ctrl-C ./socket 就没这问题了。

然而,在现实世界中,我们不仅需要给客户端发消息,我们还要能在客户端读消息。

答案就是recv()函数。





需要注意下边几点:

1.接受到的字符串并不是以'\0'结尾的

2.当用户在telnet输入文本并按了回车后,接受到的字符串是以'\r\n'结尾的

3.recv() 返回字符串的个数,如果发生错误就返回-1,如果客户端关闭了链接就返回0

4.recv()调用不一定能一次性收到所有的字符串,可能分几次返回也就是多次调用recv()

由于上边4所造成的需要调用多次的情况,因此recv()使用起来还是很繁琐的,最好能封装到一个方法中;

// 从客户端读取数据
int read_in(int socket, char *buf, int len) {
	char *s = buf;
	int slen = len;
	int c = (int)recv(socket, s, slen, 0);
	while ((c > 0) && (s[c-1] != '\n')) {
		s += c;
		slen -= c;
		c = (int)recv(socket, s, slen, 0);
	}

	if (c < 0) {
		return c;
	}else if (c == 0) {
		buf[0] = '\0';
	}else {
		s[c - 1] = '\0';
	}

	return len - slen;
}


下边我们就写一个服务器和客户端能够交互的程序,这个程序其实跟HTTP协议的原理很像,都是在双方必须遵守某项定好的协议前提下进行通信的。我们把上面讲的通信前的准备都封装成了单独的函数,比如

// 错误处理函数
void error(char *msg)
// 开启socket
int open_listener_socket()
// 绑定端口
void bind_to_port(int socket, int port)
// 向客户端发消息
int say(int socket, char *s)
// 处理服务中断
void handle_shutdown(int sig)
// 监听信号
int catch_signal(int sig, void (*handler)(int))
// 从客户端读取数据
int read_in(int socket, char *buf, int len)


代码如下

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

int listener_d;

// 错误处理函数
void error(char *msg) {
	fprintf(stderr, "Error: %s  %s", msg, strerror(errno));
	exit(1);
}

// 开启socket
int open_listener_socket() {
	int s = socket(PF_INET, SOCK_STREAM, 0);
	if (s == -1) {
		error("Can't open socket");
	}
	return s;
}

// 绑定端口
void bind_to_port(int socket, int port) {
	struct sockaddr_in name;
	name.sin_family = PF_INET;
	name.sin_port = (in_port_t)htons(port);
	name.sin_addr.s_addr = htonl(INADDR_ANY);
	int reuse = 1;
	if (setsockopt(socket, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(int)) == -1) {
		error("Can't set the reuse option on the socket");
	}
	int c = bind(socket, (struct sockaddr*)&name, sizeof(name));
	if (c == -1) {
		error("Can't bind to socket");
	}
}

// 向客户端发消息
int say(int socket, char *s) {
	int result = (int)send(socket, s, strlen(s), 0);
	if (result == -1) {
		fprintf(stderr, "%s: %s \n","和客户端通信发生错误",strerror(errno));
	}
	return result;
}

// 处理服务中断
void handle_shutdown(int sig) {
	if (listener_d) {
		close(listener_d);
	}
	fprintf(stderr, "Bye! \n");
	exit(0);
}

// 监听信号
int catch_signal(int sig, void (*handler)(int)) {
	// 创建一个新动作
	struct sigaction action;
	// 想让计算机调用哪个函数,这个被包装的my_custom_fun函数就叫做处理器
	action.sa_handler = handler;
	// 使用掩码过滤信号,通常会用一个空的掩码
	sigemptyset(&action.sa_mask);
	// 一些附加的标志位,置为0就行了
	action.sa_flags = 0;

	return sigaction(sig, &action, NULL);
}

// 从客户端读取数据
int read_in(int socket, char *buf, int len) {
	char *s = buf;
	int slen = len;
	int c = (int)recv(socket, s, slen, 0);
	while ((c > 0) && (s[c-1] != '\n')) {
		s += c;
		slen -= c;
		c = (int)recv(socket, s, slen, 0);
	}

	if (c < 0) {
		return c;
	}else if (c == 0) {
		buf[0] = '\0';
	}else {
		s[c - 1] = '\0';
	}

	return len - slen;
}
int main(int argc, const char * argv[]) {

	// 监听中断
	if (catch_signal(SIGINT, handle_shutdown) == -1) {
		error("Can not set the interrupt handler");
	}

	// 打开socket
	listener_d = open_listener_socket();

	// 绑定端口
	bind_to_port(listener_d, 30000);

	// 监听
	if (listen(listener_d, 1) == -1) {
		error("Can't listen");
	}

	puts("Waiting for connection");

	// 客户端
	struct sockaddr_storage client_addr;
	unsigned int addr_size = sizeof(client_addr);

	char buf[255];

	while (1) {

		// 链接
		int connect_d = accept(listener_d, (struct sockaddr*) &client_addr, &addr_size);
		if (connect_d == -1) {
			error("Can't open secondary socket");
		}

		// 子进程
		//if (!fork()) {

		// 	close(listener_d);

			if (say(connect_d, "Internet Knock-Knock Protocol Servet\r\nVersion 1.0\r\nKnock! Knock!\r\n>") != -1) {

				read_in(connect_d, buf, sizeof(buf));
				if (strncasecmp("Who's there?", buf, (2))) {
					say(connect_d, "You should say 'Who's there?' !");
				}else {
					if (say(connect_d, "Oscar\r\n>") != -1) {
						read_in(connect_d, buf, sizeof(buf));

						if (strncasecmp("Oscar who?", buf, (0))) {
							say(connect_d, "You should say 'Oscar who?' !");
						}else {
							say(connect_d, "Oscar silly question, you set a silly answer!\r\n");
						}
					}
				}

			}

		// 	close(connect_d);
		// 	exit(0);
		// }

		close(connect_d);
	}

	return 0;
}


编译并运行后



我们打开另一个终端



我们现在已经能够接受客户端的数据,并且能够按照我们自定义的协议进行通信了。

但是我们还需要想的更多,现在是和一个客户端通信,如果跟多个客户端呢?

打开我们上边代码中注释的部分,恢复后的代码是这样的

// 子进程
if (!fork()) {

	close(listener_d);

	if (say(connect_d, "Internet Knock-Knock Protocol Servet\r\nVersion 1.0\r\nKnock! Knock!\r\n>") != -1) {

		read_in(connect_d, buf, sizeof(buf));
		if (strncasecmp("Who's there?", buf, (2))) {
			say(connect_d, "You should say 'Who's there?' !");
		}else {
			if (say(connect_d, "Oscar\r\n>") != -1) {
				read_in(connect_d, buf, sizeof(buf));

				if (strncasecmp("Oscar who?", buf, (0))) {
					say(connect_d, "You should say 'Oscar who?' !");
				}else {
					say(connect_d, "Oscar silly question, you set a silly answer!\r\n");
				}
			}
		}

	}

	close(connect_d);
	exit(0);
}


通过对比可以看出,当我们接受到客户端的数据的时候,我们创建一个子进程,这样我们就只使用父进程监听连接,子进程处理各自的任务了

多打开几个终端试试。





。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。

到这里我们已经能够写服务器端的代码了,能够发消息和接受消息。

但这远远不够,我现在就想手写一个客户端,通过我的请求能够获取服务器端的某些数据。这其实也很简单

这时候主动权就在我们手里了。

客户端和服务器段都是用套接字来进行通信,但是两者获取套接字的方式不同。

服务器端使用的是BLAM :服务器连接网络分为四部曲:①绑定(Bind) ②监听(Listen) ③接受(Accept) ④开始(Begin)

客户端只需要两步就可以了 ①连接远程端口 ②开始通信

服务器在网络连接时必须决定使用哪个端口,而客户端除了要端口号还需要知道远程服务器的IP地址



但是这样太不容易记忆了,人们更喜欢使用域名:www.baidu.com









接下来就让我们编写一段代码。实现网络请求的任务,下边的代码需要能够连接外网才行,也就是需要FQ

#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <netdb.h>

// 错误处理函数
void error(char *msg) {
	fprintf(stderr, "Error: %s  %s", msg, strerror(errno));
	exit(1);
}

// 向客户端发消息
int say(int socket, char *s) {
	int result = (int)send(socket, s, strlen(s), 0);
	if (result == -1) {
		fprintf(stderr, "%s: %s \n","和客户端通信发生错误",strerror(errno));
	}
	return result;
}

// 根据域名和端口开启socket
int open_socket(char *host, char *port) {

	struct addrinfo *res;
	struct addrinfo hints;
	memset(&hints, 0, sizeof(hints));
	hints.ai_family = PF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	if (getaddrinfo(host, port, &hints, &res) == -1) {
		error("Can't resolve the address");
	}

	int d_sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
	if (d_sock == -1) {
		error("Can't open socket");
	}

	int c = connect(d_sock, res->ai_addr, res->ai_addrlen);
	if (c == -1) {
		error("Can't connect to socket");
	}

	return d_sock;
}

int main(int argc, const char * argv[]) {

	int d_sock;
	d_sock = open_socket("en.wikipedia.org", "80");

	char buf[255];
	sprintf(buf, "GET /wiki/%s http/1.1\r\n",argv[1]);

	say(d_sock, buf);
	say(d_sock, "Host: en.wikipedia.org\r\n\r\n");

	char rec[256];
	int bytesRcvd = recv(d_sock, rec, 255, 0);
	while (bytesRcvd) {
		if (bytesRcvd == -1) {
			error("Can't read from server");
		}

		rec[bytesRcvd] = '\0';
		printf("%s",rec);
		bytesRcvd = recv(d_sock, rec, 255, 0);
	}

	close(d_sock);

	return 0;
}


Link: http://www.asm32.net/article_details.aspx?id=7306


浏览次数 0 发布时间 2018/5/4 18:17:42 从属分类 C/C++ 【评论】【 】【打印】【关闭
 
| www.asm32.net | 2006版 | 资料中心 | linux | asm/asm32 | C/C++ | VC++ | java | Python | 书签 | ASP.Net书签 | 京ICP备09029108号-1