c 网络与套接字socket
我们已经知道如何使用I/O与文件通信,还知道了如何让同一计算机上的两个进程进行通信,这篇文章将创建具有服务器和客户端功能的程序
互联网中大部分的底层网络代码都是用C语言写的。 网络程序通常有两部分组成:服务器和客户端。
工具介绍: telnet
为了测试功能,我们使用一个叫做telnet的客户端程序连接服务器,telnet 接受两个参数:一个是服务器地址,另一个是服务器运行的端口号,
如果在运行服务器的那台计算机上运行telnet,地址可填写127.0.0.1
这样使用:假设端口号是30000
telnet 127.0.0.1
我们先说服务器这一端的:
服务器连接网络分为四部曲:①绑定(Bind) ②监听(Listen) ③接受(Accept) ④开始(Begin)
把每个首字母连起来就是BLAM
如果想写一个与网络通信的程序,就需要一种新的数据流---套接字
#include <sys/socket.h>
int listener_d = socket(PF_INET, SOCK_STREAM, );
if (listener_d == -) {
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();
name.sin_addr.s_addr = htonl(INADDR_ANY);
int c = bind(listener_d, (struct sockaddr *)&name, sizeof(name));
if (c == -) {
error("无法绑定端口");
}
2.监听(Listen)
通常会有很多客户端连接到服务器,如果我们想要客户端排队等待连接,就要使用listen()来告诉操作系统你希望队列有多长。
// 监听
if (listen(listener_d, ) == -) {
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 == -) {
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), ) == -) {
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();
} int main(int argc, const char * argv[]) { char *advice[] = {
"你为什么这么帅!\r\n",
"有没有人夸过你帅?",
"傻逼牛头,笨鳖",
"牛,你是第六人吗?",
"拔插座了吧"}; // 打开
int listener_d = socket(PF_INET, SOCK_STREAM, );
if (listener_d == -) {
error("不能打开套接字");
} // int reuse = ;
// if (setsockopt(listener_d, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(int)) == -) {
// error("无法设置套接字的“重新使用端口”选项");
// }
// 绑定端口
struct sockaddr_in name;
name.sin_family = PF_INET;
name.sin_port = (in_port_t)htons();
name.sin_addr.s_addr = htonl(INADDR_ANY);
int c = bind(listener_d, (struct sockaddr *)&name, sizeof(name));
if (c == -) {
error("无法绑定端口");
} // 监听
if (listen(listener_d, ) == -) {
error("无法监听");
} puts("等待链接..."); while () {
// 接受链接
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 == -) {
error("无法打开副套接字");
} // 通信
// char *msg = "Internet Knock-Knock Protocol Server\r\nVersion 1.0\r\nKnock! Knock!\r\n>";
char *msg = advice[rand() % ];
if (send(connect_d, msg, strlen(msg), ) == -) {
error("send");
} if (close(connect_d) == -) {
error("无法关闭链接");
} } return ;
}
// Mac 下编译运行
gcc socket.c -o socket ./socket
终端显示成这样
我们打开另一个终端来模拟客户端
太棒了,服务器和客户端能够连接且服务器能够给客户端发送数据了,但是这样的程序还是有问题的,当我们快速使用Ctrl-C结束服务器的程序,在用./socket打开机会出现这样的错误
为什么会出现这个错误呢?因为绑定端口是有延时的。
当你在某个端口绑定了一个程序,系统不允许在30秒内再绑定其他的程序,也包括上一次绑定这个端口的程序。只要在绑定前设置套接字的某个选项就能解决这个问题
把上边的代码注释的地方打开
int reuse = ;
if (setsockopt(listener_d, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(int)) == -) {
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, );
while ((c > ) && (s[c-] != '\n')) {
s += c;
slen -= c;
c = (int)recv(socket, s, slen, );
} if (c < ) {
return c;
}else if (c == ) {
buf[] = '\0';
}else {
s[c - ] = '\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();
} // 开启socket
int open_listener_socket() {
int s = socket(PF_INET, SOCK_STREAM, );
if (s == -) {
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 = ;
if (setsockopt(socket, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(int)) == -) {
error("Can't set the reuse option on the socket");
}
int c = bind(socket, (struct sockaddr*)&name, sizeof(name));
if (c == -) {
error("Can't bind to socket");
}
} // 向客户端发消息
int say(int socket, char *s) {
int result = (int)send(socket, s, strlen(s), );
if (result == -) {
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();
} // 监听信号
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 = ; 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, );
while ((c > ) && (s[c-] != '\n')) {
s += c;
slen -= c;
c = (int)recv(socket, s, slen, );
} if (c < ) {
return c;
}else if (c == ) {
buf[] = '\0';
}else {
s[c - ] = '\0';
} return len - slen;
}
int main(int argc, const char * argv[]) { // 监听中断
if (catch_signal(SIGINT, handle_shutdown) == -) {
error("Can not set the interrupt handler");
} // 打开socket
listener_d = open_listener_socket(); // 绑定端口
bind_to_port(listener_d, ); // 监听
if (listen(listener_d, ) == -) {
error("Can't listen");
} puts("Waiting for connection"); // 客户端
struct sockaddr_storage client_addr;
unsigned int addr_size = sizeof(client_addr); char buf[]; while () { // 链接
int connect_d = accept(listener_d, (struct sockaddr*) &client_addr, &addr_size);
if (connect_d == -) {
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>") != -) { read_in(connect_d, buf, sizeof(buf));
if (strncasecmp("Who's there?", buf, ())) {
say(connect_d, "You should say 'Who's there?' !");
}else {
if (say(connect_d, "Oscar\r\n>") != -) {
read_in(connect_d, buf, sizeof(buf)); if (strncasecmp("Oscar who?", buf, ())) {
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();
// } close(connect_d);
} return ;
}
编译并运行后
我们打开另一个终端
我们现在已经能够接受客户端的数据,并且能够按照我们自定义的协议进行通信了。
但是我们还需要想的更多,现在是和一个客户端通信,如果跟多个客户端呢?
打开我们上边代码中注释的部分,恢复后的代码是这样的
// 子进程
if (!fork()) { close(listener_d); if (say(connect_d, "Internet Knock-Knock Protocol Servet\r\nVersion 1.0\r\nKnock! Knock!\r\n>") != -) { read_in(connect_d, buf, sizeof(buf));
if (strncasecmp("Who's there?", buf, ())) {
say(connect_d, "You should say 'Who's there?' !");
}else {
if (say(connect_d, "Oscar\r\n>") != -) {
read_in(connect_d, buf, sizeof(buf)); if (strncasecmp("Oscar who?", buf, ())) {
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();
}
通过对比可以看出,当我们接受到客户端的数据的时候,我们创建一个子进程,这样我们就只使用父进程监听连接,子进程处理各自的任务了
多打开几个终端试试。
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
到这里我们已经能够写服务器端的代码了,能够发消息和接受消息。
但这远远不够,我现在就想手写一个客户端,通过我的请求能够获取服务器端的某些数据。这其实也很简单
这时候主动权就在我们手里了。
客户端和服务器段都是用套接字来进行通信,但是两者获取套接字的方式不同。
服务器端使用的是BLAM :服务器连接网络分为四部曲:①绑定(Bind) ②监听(Listen) ③接受(Accept) ④开始(Begin)
客户端只需要两步就可以了 ①连接远程端口 ②开始通信
服务器在网络连接时必须决定使用哪个端口,而客户端除了要端口号还需要知道远程服务器的IP地址
但是这样太不容易记忆了,人们更喜欢使用域名:www.baidu.com
接下来就让我们编写一段代码。实现网络请求的任务,下边的代码需要能够连接外网才行,也就是需要翻墙
#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();
} // 向客户端发消息
int say(int socket, char *s) {
int result = (int)send(socket, s, strlen(s), );
if (result == -) {
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, , sizeof(hints));
hints.ai_family = PF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo(host, port, &hints, &res) == -) {
error("Can't resolve the address");
} int d_sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (d_sock == -) {
error("Can't open socket");
} int c = connect(d_sock, res->ai_addr, res->ai_addrlen);
if (c == -) {
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", ""); char buf[];
sprintf(buf, "GET /wiki/%s http/1.1\r\n",argv[]); say(d_sock, buf);
say(d_sock, "Host: en.wikipedia.org\r\n\r\n"); char rec[];
int bytesRcvd = recv(d_sock, rec, , );
while (bytesRcvd) {
if (bytesRcvd == -) {
error("Can't read from server");
} rec[bytesRcvd] = '\0';
printf("%s",rec);
bytesRcvd = recv(d_sock, rec, , );
} close(d_sock); return ;
}
c 网络与套接字socket的更多相关文章
- Java网络编程--套接字Socket
一.套接字Socket IP地址标志Internet上的计算机,端口号标志正在计算机上运行的进程(程序). 端口号被规定为一个16位的0--65535之间的整数,其中,0--1023被预先定义的服务通 ...
- 网络编程 套接字socket TCP UDP
网络编程与套接字 网络编程 网络编程是什么: 网络通常指的是计算机中的互联网,是由多台计算机通过网线或其他媒介相互链接组成的 编写基于网络的应用程序的过程序称之为网络编程. 网络编程最主要的工 ...
- 网络---中断套接字Socket
package socketpack_2; import java.awt.BorderLayout; import java.awt.EventQueue; import java.awt.even ...
- 传输模型,网络层次划分,三次握手,四次挥手,IP与端口,套接字socket
了解套接字之前,需要先了解基本的传输模型 其次,还需要了解网络的七层划分和四层结构 在python中,数据链路层相当于硬件层,python不需要了解,只用在传输层进行学习就足够了 其中,传输层分为TC ...
- 网络编程(二)--TCP协议、基于tcp协议的套接字socket
一.TCP协议(Transmission Control Protocol 传输控制协议) 1.可靠传输,TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会 ...
- 网络编程(二)——TCP协议、基于tcp协议的套接字socket
TCP协议与基于tcp协议的套接字socket 一.TCP协议(流式协议) 1.可靠传输,TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的 ...
- Linux进程间通信(八):流套接字 socket()、bind()、listen()、accept()、connect()、read()、write()、close()
前面说到的进程间的通信,所通信的进程都是在同一台计算机上的,而使用socket进行通信的进程可以是同一台计算机的进程,也是可以是通过网络连接起来的不同计算机上的进程.通常我们使用socket进行网络编 ...
- Linux进程间通信(九):数据报套接字 socket()、bind()、sendto()、recvfrom()、close()
前一篇文章,Linux进程间通信——使用流套接字介绍了一些有关socket(套接字)的一些基本内容,并讲解了流套接字的使用,这篇文章将会给大家讲讲,数据报套接字的使用. 一.简单回顾——什么是数据报套 ...
- 套接字socket 的地址族和类型、工作原理、创建过程
注:本分类下文章大多整理自<深入分析linux内核源代码>一书,另有参考其他一些资料如<linux内核完全剖析>.<linux c 编程一站式学习>等,只是为了更好 ...
随机推荐
- 探索ASP.NET MVC5系列之~~~1.基础篇---必须知道的小技能
其实任何资料里面的任何知识点都无所谓,都是不重要的,重要的是学习方法,自行摸索的过程 汇总:http://www.cnblogs.com/dunitian/p/4822808.html#mvc 本章D ...
- Word/Excel 在线预览
前言 近日项目中做到一个功能,需要上传附件后能够在线预览.之前也没做过这类似的,于是乎就查找了相关资料,.net实现Office文件预览大概有这几种方式: ① 使用Microsoft的Office组件 ...
- Bootstrap 模态框(Modal)插件
页面效果: html+js: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> ...
- Android数据加密之MD5加密
前言: 项目中无论是密码的存储或者说判断文件是否是同一文件,都会用到MD5算法,今天来总结一下MD5加密算法. 什么是MD5加密? MD5英文全称“Message-Digest Algorithm 5 ...
- InstallShield 脚本语言学习笔记
InstallShield脚本语言是类似C语言,利用InstallShield的向导或模板都可以生成基本的脚本程序框架,可以在此基础上按自己的意愿进行修改和添加. 一.基本语法规则 ...
- Spring Enable annotation – writing a custom Enable annotation
原文地址:https://www.javacodegeeks.com/2015/04/spring-enable-annotation-writing-a-custom-enable-annotati ...
- 【绝对干货】仿微信QQ设置图形头像裁剪,让你的App从此炫起来~
最近在做毕业设计,想有一个功能和QQ一样可以裁剪头像并设置圆形头像,额,这是设计狮的一种潮流. 而纵观现在主流的APP,只要有用户系统这个功能,这个需求一般都是在(bu)劫(de)难(bu)逃(xue ...
- ASP.NET MVC关于Ajax以及Jquery的无限级联动
---恢复内容开始--- 第一次发表博文,发表博文的目的是巩固自己的技术,也能够共享给大家.写的不好的地方,希望大家多给给意见.老司机勿喷 数据结构() NewsTypeId 新闻ID, NewsTy ...
- BPM应用开发解决方案分享
一.需求分析企业整体管理是一个完整的体系,如果 把这个体系比做一个拼图,企业信息化通过各个业务系统覆盖了一部分业务. 企业通过采购实施通用软件的方式,覆盖了企业的核心业务和专业化业务然而系统只满足了部 ...
- Android—关于自定义对话框的工具类
开发中有很多地方会用到自定义对话框,为了避免不必要的城府代码,在此总结出一个工具类. 弹出对话框的地方很多,但是都大同小异,不同无非就是提示内容或者图片不同,下面这个类是将提示内容和图片放到了自定义函 ...