我们已经知道如何使用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");
}

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

 1 #include <stdio.h>
2 #include <sys/socket.h>
3 #include <arpa/inet.h>
4 #include <string.h>
5 #include <errno.h>
6 #include <stdlib.h>
7 #include <unistd.h>
8
9 void error(char *msg) {
10 fprintf(stderr, "Error: %s %s", msg, strerror(errno));
11 exit(1);
12 }
13
14
15 int main(int argc, const char * argv[]) {
16
17
18 char *advice[] = {
19 "你为什么这么帅!\r\n",
20 "有没有人夸过你帅?",
21 "傻逼牛头,笨鳖",
22 "牛,你是第六人吗?",
23 "拔插座了吧"};
24
25
26 // 打开
27 int listener_d = socket(PF_INET, SOCK_STREAM, 0);
28 if (listener_d == -1) {
29 error("不能打开套接字");
30 }
31
32 // int reuse = 1;
33 // if (setsockopt(listener_d, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(int)) == -1) {
34 // error("无法设置套接字的“重新使用端口”选项");
35 // }
36 // 绑定端口
37 struct sockaddr_in name;
38 name.sin_family = PF_INET;
39 name.sin_port = (in_port_t)htons(30000);
40 name.sin_addr.s_addr = htonl(INADDR_ANY);
41 int c = bind(listener_d, (struct sockaddr *)&name, sizeof(name));
42 if (c == -1) {
43 error("无法绑定端口");
44 }
45
46 // 监听
47 if (listen(listener_d, 10) == -1) {
48 error("无法监听");
49 }
50
51 puts("等待链接...");
52
53 while (1) {
54 // 接受链接
55 struct sockaddr_storage client_addr; // 保存链接客户端的相信信息
56 unsigned int address_size = sizeof(client_addr);
57 int connect_d = accept(listener_d, (struct sockaddr *)&client_addr, &address_size);
58 if (connect_d == -1) {
59 error("无法打开副套接字");
60 }
61
62 // 通信
63 // char *msg = "Internet Knock-Knock Protocol Server\r\nVersion 1.0\r\nKnock! Knock!\r\n>";
64 char *msg = advice[rand() % 5];
65 if (send(connect_d, msg, strlen(msg), 0) == -1) {
66 error("send");
67 }
68
69 if (close(connect_d) == -1) {
70 error("无法关闭链接");
71 }
72
73 }
74
75
76 return 0;
77 }
// 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()使用起来还是很繁琐的,最好能封装到一个方法中;

 1 // 从客户端读取数据
2 int read_in(int socket, char *buf, int len) {
3 char *s = buf;
4 int slen = len;
5 int c = (int)recv(socket, s, slen, 0);
6 while ((c > 0) && (s[c-1] != '\n')) {
7 s += c;
8 slen -= c;
9 c = (int)recv(socket, s, slen, 0);
10 }
11
12 if (c < 0) {
13 return c;
14 }else if (c == 0) {
15 buf[0] = '\0';
16 }else {
17 s[c - 1] = '\0';
18 }
19
20 return len - slen;
21 }

下边我们就写一个服务器和客户端能够交互的程序,这个程序其实跟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)

代码如下

  1 #include <stdio.h>
2 #include <sys/socket.h>
3 #include <arpa/inet.h>
4 #include <string.h>
5 #include <errno.h>
6 #include <stdlib.h>
7 #include <unistd.h>
8 #include <signal.h>
9
10 int listener_d;
11
12 // 错误处理函数
13 void error(char *msg) {
14 fprintf(stderr, "Error: %s %s", msg, strerror(errno));
15 exit(1);
16 }
17
18 // 开启socket
19 int open_listener_socket() {
20 int s = socket(PF_INET, SOCK_STREAM, 0);
21 if (s == -1) {
22 error("Can't open socket");
23 }
24 return s;
25 }
26
27 // 绑定端口
28 void bind_to_port(int socket, int port) {
29 struct sockaddr_in name;
30 name.sin_family = PF_INET;
31 name.sin_port = (in_port_t)htons(port);
32 name.sin_addr.s_addr = htonl(INADDR_ANY);
33 int reuse = 1;
34 if (setsockopt(socket, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(int)) == -1) {
35 error("Can't set the reuse option on the socket");
36 }
37 int c = bind(socket, (struct sockaddr*)&name, sizeof(name));
38 if (c == -1) {
39 error("Can't bind to socket");
40 }
41 }
42
43 // 向客户端发消息
44 int say(int socket, char *s) {
45 int result = (int)send(socket, s, strlen(s), 0);
46 if (result == -1) {
47 fprintf(stderr, "%s: %s \n","和客户端通信发生错误",strerror(errno));
48 }
49 return result;
50 }
51
52 // 处理服务中断
53 void handle_shutdown(int sig) {
54 if (listener_d) {
55 close(listener_d);
56 }
57 fprintf(stderr, "Bye! \n");
58 exit(0);
59 }
60
61 // 监听信号
62 int catch_signal(int sig, void (*handler)(int)) {
63 // 创建一个新动作
64 struct sigaction action;
65 // 想让计算机调用哪个函数,这个被包装的my_custom_fun函数就叫做处理器
66 action.sa_handler = handler;
67 // 使用掩码过滤信号,通常会用一个空的掩码
68 sigemptyset(&action.sa_mask);
69 // 一些附加的标志位,置为0就行了
70 action.sa_flags = 0;
71
72 return sigaction(sig, &action, NULL);
73 }
74
75 // 从客户端读取数据
76 int read_in(int socket, char *buf, int len) {
77 char *s = buf;
78 int slen = len;
79 int c = (int)recv(socket, s, slen, 0);
80 while ((c > 0) && (s[c-1] != '\n')) {
81 s += c;
82 slen -= c;
83 c = (int)recv(socket, s, slen, 0);
84 }
85
86 if (c < 0) {
87 return c;
88 }else if (c == 0) {
89 buf[0] = '\0';
90 }else {
91 s[c - 1] = '\0';
92 }
93
94 return len - slen;
95 }
96 int main(int argc, const char * argv[]) {
97
98 // 监听中断
99 if (catch_signal(SIGINT, handle_shutdown) == -1) {
100 error("Can not set the interrupt handler");
101 }
102
103 // 打开socket
104 listener_d = open_listener_socket();
105
106 // 绑定端口
107 bind_to_port(listener_d, 30000);
108
109 // 监听
110 if (listen(listener_d, 1) == -1) {
111 error("Can't listen");
112 }
113
114 puts("Waiting for connection");
115
116 // 客户端
117 struct sockaddr_storage client_addr;
118 unsigned int addr_size = sizeof(client_addr);
119
120 char buf[255];
121
122 while (1) {
123
124 // 链接
125 int connect_d = accept(listener_d, (struct sockaddr*) &client_addr, &addr_size);
126 if (connect_d == -1) {
127 error("Can't open secondary socket");
128 }
129
130 // 子进程
131 //if (!fork()) {
132
133 // close(listener_d);
134
135 if (say(connect_d, "Internet Knock-Knock Protocol Servet\r\nVersion 1.0\r\nKnock! Knock!\r\n>") != -1) {
136
137 read_in(connect_d, buf, sizeof(buf));
138 if (strncasecmp("Who's there?", buf, (2))) {
139 say(connect_d, "You should say 'Who's there?' !");
140 }else {
141 if (say(connect_d, "Oscar\r\n>") != -1) {
142 read_in(connect_d, buf, sizeof(buf));
143
144 if (strncasecmp("Oscar who?", buf, (0))) {
145 say(connect_d, "You should say 'Oscar who?' !");
146 }else {
147 say(connect_d, "Oscar silly question, you set a silly answer!\r\n");
148 }
149 }
150 }
151
152 }
153
154 // close(connect_d);
155 // exit(0);
156 // }
157
158 close(connect_d);
159 }
160
161
162 return 0;
163 }

编译并运行后

我们打开另一个终端

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

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

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

 1  // 子进程
2 if (!fork()) {
3
4 close(listener_d);
5
6 if (say(connect_d, "Internet Knock-Knock Protocol Servet\r\nVersion 1.0\r\nKnock! Knock!\r\n>") != -1) {
7
8 read_in(connect_d, buf, sizeof(buf));
9 if (strncasecmp("Who's there?", buf, (2))) {
10 say(connect_d, "You should say 'Who's there?' !");
11 }else {
12 if (say(connect_d, "Oscar\r\n>") != -1) {
13 read_in(connect_d, buf, sizeof(buf));
14
15 if (strncasecmp("Oscar who?", buf, (0))) {
16 say(connect_d, "You should say 'Oscar who?' !");
17 }else {
18 say(connect_d, "Oscar silly question, you set a silly answer!\r\n");
19 }
20 }
21 }
22
23 }
24
25 close(connect_d);
26 exit(0);
27 }

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

多打开几个终端试试。

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

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

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

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

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

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

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

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

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

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

 1 #include <stdio.h>
2 #include <sys/socket.h>
3 #include <arpa/inet.h>
4 #include <string.h>
5 #include <errno.h>
6 #include <stdlib.h>
7 #include <unistd.h>
8 #include <signal.h>
9 #include <netdb.h>
10
11
12 // 错误处理函数
13 void error(char *msg) {
14 fprintf(stderr, "Error: %s %s", msg, strerror(errno));
15 exit(1);
16 }
17
18 // 向客户端发消息
19 int say(int socket, char *s) {
20 int result = (int)send(socket, s, strlen(s), 0);
21 if (result == -1) {
22 fprintf(stderr, "%s: %s \n","和客户端通信发生错误",strerror(errno));
23 }
24 return result;
25 }
26
27
28
29 // 根据域名和端口开启socket
30 int open_socket(char *host, char *port) {
31
32 struct addrinfo *res;
33 struct addrinfo hints;
34 memset(&hints, 0, sizeof(hints));
35 hints.ai_family = PF_UNSPEC;
36 hints.ai_socktype = SOCK_STREAM;
37 if (getaddrinfo(host, port, &hints, &res) == -1) {
38 error("Can't resolve the address");
39 }
40
41 int d_sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
42 if (d_sock == -1) {
43 error("Can't open socket");
44 }
45
46 int c = connect(d_sock, res->ai_addr, res->ai_addrlen);
47 if (c == -1) {
48 error("Can't connect to socket");
49 }
50
51 return d_sock;
52 }
53
54
55 int main(int argc, const char * argv[]) {
56
57
58
59 int d_sock;
60 d_sock = open_socket("en.wikipedia.org", "80");
61
62 char buf[255];
63 sprintf(buf, "GET /wiki/%s http/1.1\r\n",argv[1]);
64
65 say(d_sock, buf);
66 say(d_sock, "Host: en.wikipedia.org\r\n\r\n");
67
68 char rec[256];
69 int bytesRcvd = recv(d_sock, rec, 255, 0);
70 while (bytesRcvd) {
71 if (bytesRcvd == -1) {
72 error("Can't read from server");
73 }
74
75 rec[bytesRcvd] = '\0';
76 printf("%s",rec);
77 bytesRcvd = recv(d_sock, rec, 255, 0);
78 }
79
80 close(d_sock);
81
82 return 0;
83 }

 
 

 

C语言与套接字的更多相关文章

  1. 【Socket规划】套接字Windows台C语言

    [编译环境]:Visual Studio 2013 这是服务端实现流程. #include<stdio.h> #include<stdlib.h> #include<wi ...

  2. Linux系统C语言socket tcp套接字编程

    1.套接字的地址结构: typedef uint32_t in_addr_t; //32位无符号整数,用于表示网络地址 struct in_addr{ in_addr_t s_addr; //32位 ...

  3. c 网络与套接字socket

    我们已经知道如何使用I/O与文件通信,还知道了如何让同一计算机上的两个进程进行通信,这篇文章将创建具有服务器和客户端功能的程序 互联网中大部分的底层网络代码都是用C语言写的. 网络程序通常有两部分组成 ...

  4. (十三) [终篇] 一起学 Unix 环境高级编程 (APUE) 之 网络 IPC:套接字

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  5. Java知多少(105)套接字(Socket)

    网络应用模式主要有: 主机/终端模式:集中计算,集中管理: 客户机/服务器(Client/Server,简称C/S)模式:分布计算,分布管理: 浏览器/服务器模式:利用Internet跨平台. www ...

  6. (转载)Linux 套接字编程中的 5 个隐患

    在 4.2 BSD UNIX® 操作系统中首次引入,Sockets API 现在是任何操作系统的标准特性.事实上,很难找到一种不支持 Sockets API 的现代语言.该 API 相当简单,但新的开 ...

  7. iOS - Socket 网络套接字

    1.Socket 网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个 Socket.Socket 又称 "套接字",应用程序通常通过 "套接字& ...

  8. 使用TCP/IP的套接字(Socket)进行通信

    http://www.cnblogs.com/mengdd/archive/2013/03/10/2952616.html 使用TCP/IP的套接字(Socket)进行通信 套接字Socket的引入 ...

  9. 套接字I/O模型-重叠I/O

    重叠模型的基本设计原理是让应用程序使用重叠的数据结构,一次投递一个或多个WinsockI/O请求.针对那些提交的请求,在它们完成之后,应用程序可为它们提供服务.模型的总体设计以Windows重叠I/O ...

随机推荐

  1. iOS多线程之NSThread详解

    在iOS中每个进程启动后都会建立一个主线程(UI线程),这个线程是其他线程的父线程.由于iOS中除了主线程,其他子线程是独立于Cocoa Touch的,所以只有主线程可以更新UI界面.iOS多线程的使 ...

  2. webstrom快捷键速查

    编辑 Ctrl + Space 基本代码完成 (任何类. 方法或变量名称)Ctrl + Shift + Enter 完整的语句Ctrl + P (在方法调用参数) 内的参数信息Ctrl + Q 快速的 ...

  3. Delphi项目构成之单元文件PAS

    单元文件是Pascal源文件,扩展名为.pas. 有三种类型的单元文件: 窗体/数据模块和框架的单元文件(form/data module and frame units),一般由Delphi自动生成 ...

  4. 查看mysql表结构和表创建语句的方法(转)

    查看mysql表结构的方法有三种:1.desc tablename;例如:要查看jos_modules表结构的命令:desc jos_modules;查看结果:mysql> desc jos_m ...

  5. tomcat配置文件详解

    Tomcat系列之服务器的安装与配置以及各组件详解   tomcat 配置文件详解

  6. listview+seekbar问题的解决

    最近做了个项目,其中有录音播放功能.每次录音结束都会在listView中显示,在listView中能播放每次的录音,也可以每条录音之间的切换播放.随之就引发了许多的问题,比如当我播放第一条录音的时所有 ...

  7. JS添加DOM元素CSS权重BUG

    修改删除table的时候,比如拆分合并单元格,合并全部TR中的某个TD后在拆分还原,即使直接在td标签中设置了td的高宽属性,当td在css文件中设置为宽度auto的时候,不能显示出TD来,显示TD宽 ...

  8. codevs 1051 接龙游戏

    codevs 1051 接龙游戏 http://codevs.cn/problem/1051/ 题目描述 Description 给出了N个单词,已经按长度排好了序.如果某单词i是某单词j的前缀,i- ...

  9. rpc框架之 thrift 学习 1 - 安装 及 hello world

    thrift是一个facebook开源的高效RPC框架,其主要特点是跨语言及二进制高效传输(当然,除了二进制,也支持json等常用序列化机制),官网地址:http://thrift.apache.or ...

  10. Java多线程之Runable与Thread

    Java多线程是Java开发中的基础内容,但是涉及到高并发就有很深的研究可做了. 最近看了下<Java并发实战>,发先有些地方,虽然可以理解,但是自己在应用中很难下手. 所以还是先回顾一下 ...