不得不说的异步编程,不得不说异步编程

WinPcap编程(三),winpcap编程

1.过滤器设置

  设置过滤器,得到你想要的哪种类型的包。Like WireShark。

  过程:编译过滤器,然后设置过滤器。直接上参考文档的代码:

  

图片 1 if
(d->addresses != NULL) /* 获取接口第一个地址的掩码 */
netmask=((struct sockaddr_in
*)(d->addresses->netmask))->sin_addr.S_un.S_addr; else /*
如果这个接口没有地址,那么我们假设这个接口在C类网络中 */
netmask=0xffffff; compile the filter if (pcap_compile(adhandle, &fcode,
“ip and tcp”, 1, netmask) < 0) { fprintf(stderr,”\nUnable to compile
the packet filter. Check the syntax.\n”); /* 释放设备列表 */
pcap_freealldevs(alldevs); return -1; } set the filter if
(pcap_setfilter(adhandle, &fcode) < 0) { fprintf(stderr,”\nError
setting the filter.\n”); /* 释放设备列表 */
pcap_freealldevs(alldevs); return -1; } View Code

 

2.分析数据包

  只需要知道怎么构造,然后怎么处理就可以了。

 

源码:

图片 2#define
WIN32 #include “pcap.h” typedef struct mac{ u_char byte1; u_char
byte2; u_char byte3; u_char byte4; u_char byte5; u_char byte6; }mac;
typedef struct eth_header{ mac dmac; mac smac; u_short type;
}eth_header; /* 4字节的IP地址 */ typedef struct ip_address{ u_char
byte1; u_char byte2; u_char byte3; u_char byte4; }ip_address; /*
IPv4 首部 */ typedef struct ip_header{ u_char ver_ihl; // 版本 (4
bits) + 首部长度 (4 bits) u_char tos; // 服务类型(Type of service)
u_short tlen; // 总长(Total length) u_short identification; //
标识(Identification) u_short flags_fo; // 标志位(Flags) (3 bits) +
段偏移量(Fragment offset) (13 bits) u_char ttl; // 存活时间(Time to
live) u_char proto; // 协议(Protocol) u_short crc; //
首部校验和(Header checksum) ip_address saddr; // 源地址(Source address)
ip_address daddr; // 目的地址(Destination address) u_int op_pad; //
选项与填充(Option + Padding) }ip_header; /* UDP 首部*/ typedef struct
udp_header{ u_short sport; // 源端口(Source port) u_short dport; //
目的端口(Destination port) u_short len; // UDP数据包长度(Datagram
length) u_short crc; // 校验和(Checksum) }udp_header; /* 回调函数原型
*/ void packet_handler2(u_char *param, const struct pcap_pkthdr
*header, const u_char *pkt_data); int main() { pcap_if_t
*alldevs; pcap_if_t *d; int inum; int i = 0; pcap_t *adhandle;
char errbuf[PCAP_ERRBUF_SIZE]; u_int netmask; char
packet_filter[] = “ip”; struct bpf_program fcode; /* 获得设备列表
*/ if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs,
errbuf) == -1) { fprintf(stderr, “Error in pcap_findalldevs: %s\n”,
errbuf); exit(1); } /* 打印列表 */ for (d = alldevs; d; d =
d->next) { printf(“%d. %s”, ++i, d->name); if (d->description)
printf(” (%s)\n”, d->description); else printf(” (No description
available)\n”); } if (i == 0) { printf(“\nNo interfaces found! Make
sure WinPcap is installed.\n”); return -1; } printf(“Enter the
interface number (1-%d):”, i); scanf_s(“%d”, &inum); if (inum < 1 ||
inum > i) { printf(“\nInterface number out of range.\n”); /*
释放设备列表 */ pcap_freealldevs(alldevs); return -1; } /*
跳转到已选设备 */ for (d = alldevs, i = 0; i< inum – 1; d =
d->next, i++); /* 打开适配器 */ //if ((adhandle =
pcap_open(d->name, // 设备名 // 65536, // 要捕捉的数据包的部分 // //
65535保证能捕获到不同数据链路层上的每个数据包的全部内容 //
PCAP_OPENFLAG_PROMISCUOUS, // 混杂模式 // 0, // 读取超时时间 // NULL,
// 远程机器验证 // errbuf // 错误缓冲池 // )) == NULL) if ((adhandle =
pcap_open_live(d->name, 65536,
PCAP_OPENFLAG_PROMISCUOUS,0,errbuf)) == NULL) { fprintf(stderr,
“\nUnable to open the adapter. %s is not supported by WinPcap\n”); /*
释放设备列表 */ pcap_freealldevs(alldevs); return -1; } /*
检查数据链路层,为了简单,我们只考虑以太网 */ //if
(pcap_datalink(adhandle) != DLT_EN10MB) //{ // fprintf(stderr,
“\nThis program works only on Ethernet networks.\n”); // /*
释放设备列表 */ // pcap_freealldevs(alldevs); // return -1; //} if
(d->addresses != NULL) /* 获得接口第一个地址的掩码 */ netmask =
((struct sockaddr_in
*)(d->addresses->netmask))->sin_addr.S_un.S_addr; else /*
如果接口没有地址,那么我们假设一个C类的掩码 */ netmask = 0xffffff;
//编译过滤器 if (pcap_compile(adhandle, &fcode, packet_filter, 1,
netmask) <0) { fprintf(stderr, “\nUnable to compile the packet
filter. Check the syntax.\n”); /* 释放设备列表 */
pcap_freealldevs(alldevs); return -1; } //设置过滤器 if
(pcap_setfilter(adhandle, &fcode)<0) { fprintf(stderr, “\nError
setting the filter.\n”); /* 释放设备列表 */
pcap_freealldevs(alldevs); return -1; } printf(“\nlistening on
%s…\n”, d->description); /* 释放设备列表 */
pcap_freealldevs(alldevs); /* 开始捕捉 */ pcap_loop(adhandle, 0,
packet_handler2, NULL); system(“pause”); return 0; } /*
回调函数,当收到每一个数据包时会被libpcap所调用 */ void
packet_handler2(u_char *param, const struct pcap_pkthdr *header,
const u_char *pkt_data) { struct tm ltime; char timestr[16];
time_t local_tv_sec; eth_header *eh; ip_header *ih; udp_header
*uh; u_int ip_len; u_short sport=0, dport=0; int i; /*
将时间戳转换成可识别的格式 */ local_tv_sec = header->ts.tv_sec;
localtime_s(&ltime,&local_tv_sec); strftime(timestr, sizeof timestr,
“%H:%M:%S”, &ltime); /* 打印数据包的时间戳和长度 */ printf(“Time
Stamp:%s.%.6d \nLength:%d \n”, timestr, header->ts.tv_usec,
header->len); ///*获得以太网帧的首部*/ //eh = (eth_header
*)(pkt_data); ///* 获得IP数据包头部的位置 */ //ih = (ip_header
*)(pkt_data + // 14); //以太网头部长度 ///* 获得UDP首部的位置
//IP数据报头部是4bits,单位是32bit(4个字节)
//一个IP包头的长度最长为“1111”,即15*4=60个字节。IP包头最小长度为20字节。
//*/ //ip_len = (ih->ver_ihl & 0xf) * 4; //uh = (udp_header
*)((u_char*)ih + ip_len); ///* 将网络字节序列转换成主机字节序列 */
//sport = ntohs(uh->sport); //dport = ntohs(uh->dport);
/*打印数据包的数据*/ printf(“\nDATA:”); for (i = 0; i<
header->len; ++i) { printf(” %02x”, pkt_data[i]); if ((i + 1) % 16
== 0) { printf(“\n”); } } /*打印MAC地址*/ //printf(“\nDestination:
%02x-%02x-%02x-%02x-%02x-%02x”, // eh->dmac.byte1, //
eh->dmac.byte2, // eh->dmac.byte3, // eh->dmac.byte4, //
eh->dmac.byte5, // eh->dmac.byte6); //printf(“\nResources:
%02x-%02x-%02x-%02x-%02x-%02x”, // eh->smac.byte1, //
eh->smac.byte2, // eh->smac.byte3, // eh->smac.byte4, //
eh->smac.byte5, // eh->smac.byte6); ///* 打印IP地址和UDP端口 */
//printf(“\nIP ADDRESS:%d.%d.%d.%d:%d -> %d.%d.%d.%d:%d\nUDP
LENGTH:%d\n”, // ih->saddr.byte1, // ih->saddr.byte2, //
ih->saddr.byte3, // ih->saddr.byte4, // sport, //
ih->daddr.byte1, // ih->daddr.byte2, // ih->daddr.byte3, //
ih->daddr.byte4, // dport, // ih->tlen); } View Code

 

3.发送包比较简单,就不多说了。

1.过滤器设置
设置过滤器,得到你想要的哪种类型的包。Like WireShark。
过程:编译过滤器,然后设置过滤器。…

不得不说的异步编程,不得不说异步编程

1、什么是异步编程?

   
异步编程就是把耗时的操作放进一个单独的线程中进行处理(该线程需要将执行进度反映到界面上)。由于耗时操作是在另外一个线程中被执行的,所以它不会堵塞主线程。主线程开启这些单独的线程后,还可以继续执行其他操作(例如窗体绘制等)。

   
异步编程可以提高用户体验,避免在进行耗时操作时让用户看到程序“卡死”的现象。

 

2、异步编程模型(APM)

    APM是Asynchronous Programming
Mode的缩写,即异步编程模型的意思,它允许程序用更少的线程去执行更多的操作。在.NET
Framework中,要分辨某个类是否实现了异步编程模型,主要就是看该类是否实现了返回类型为IAsyncResult接口的BeginXXX方法和EndXXX方法。

   
由于委托类型定义了BeginInvoke和EndInvoke方法,所以委托类型都实现了异步编程模型。

    2.1 Beginxxx方法–开始执行异步操作

         
在需要获取文件中的内容时,我们通常会使用FileStream的同步方法Read进行读取,该同步方法的定义为:

          public override int Read(byte[] array,int offset,int count)

         
当使用上面的方法读取大文件的内容时,会出现堵塞UI线程,导致在文件内容没有读取完成之前,用户不能对窗体进行任何操作(包括关闭应用程序),这时窗体就会出现无法响应的情况。

          为了解决这个问题,微软早在.NET
1.0的时候就提出了异步编程模型,并为FileStream类提供了异步模式的方法实现,即BeginRead方法。该方法会异步地执行读取操作,并返回实现了IAsyncResult接口的对象(该对象存储这异步操作的信息)。

         
下面给出了BeginRead方法的定义,我们可以从中找出它与同步方法Read的区别:

          public override IAsyncResult BeginRead(byte[] array,int
offset,int numBytes,AsyncCallback userCallback,Object stateObject)

         
从以上的异步方法的定义可以看出,该异步方法的前面3个参数与同步方法Read一致,后两个参数userCallback和StateObject则是同步方法所不具备的。userCallback表示异步操作完成后需要回调的方法,该方法必须匹配AsyncCallback委托类型;stateObject则代表传递给回调方法的对象,在回调方法中,可以通过查询IAsyncResult接口的AsyncState属性来读取该对象。该异步方法之所以不会堵塞UI线程,是因为它在被调用后,会立即把控制权交还给调用线程(如果是UI线程调用了该方法,则就将控制权返回给UI线程),然后由另一个线程去执行文件读取操作。

 

    2.2 Endxxx方法–结束异步操作

         
每次调用Beginxxx方法后,应用程序还需调用Endxxx方法来获取操作返回的结果。Beginxxx方法所返回的,是实现了IAsyncResult接口的对象,该对象并非相应的同步方法返回的结果。此时还需要调用Endxxx方法来结束异步操作,并向该方法传递Beginxxx所返回的对象。Endxxx方法返回的类型与同步方法相同,如FileStream的EndRead方法会返回一个Int32类型,代表从文件流中实际读取的字节数。

         
Endxxx方法有许多中方式调用,但有一种是最常用的,即使用AsyncCallback委托来指定操作完成时要调用的方法,在回调方法中调用Endxxx方法来获得异步操作返回的结果。

 1 static void Main()
 2 {
 3     SynchronizationContext sc=SynchronizationContext.Current;
 4     AsyncMethodCaller methodCaller=new AsyncMethodCaller(DownLoadFileAsync);
 5     method.BeginInvoke(txtUrl.Text.Trim(),GetResult,null);
 6 }
 7 private static void GetRsult(IAsyncResult result)
 8 {
 9     AsyncMethodCaller caller=(AsyncMethodCaller)((AsyncResult)result).AsyncDelegate;
10     string returnstring=call.EndInvoke(result);
11 }

 

3、异步编程模型(EAP)

   
虽然前面的异步编程可以解决执行耗时操作时界面无法响应的问题,但APM也同样存在这一些明显的问题,如不支持对异步操作的取消以及不能提供下载进度报告等。然而对于桌面应用程序而言,进度报告和取消操作的功能是必不可少的,所以微软在.NET
2.0
发布时又提出了一个新的异步编程模型–基于事件的异步模型,即EAP(Event-based
Asynchronous Pattern)。

   
实现了EAP的类具有一个或多个以Async为后缀的方法,以及对应的Completed事件,并且这些类支持异步方法的取消和进度报告。在.NET类库中,只有部分类实现了EAP,共17个。在这17个类中,开发过程中使用最多的莫过于BackgroundWorker类了。

    经常使用的属性为:

    CancellationPending:用来指示应用程序是否已请求取消后台操作;

    IsBusy:指示异步操作是否正在运行;

    WorkReportProgress:只是BackgrounWorker能否报告进度;

   
WorkerSupportsCancellation:指示BackgroundWoker是否支持异步取消操作;

    经常使用的方法为:

    CancelAsync:请求取消异步操作;

    ReportProgress:用于引发ProgressChanged事件;

    RunWorkAsync:调用后开始执行异步操作;

    经常使用到的3个事件为:

    DoWork:调用RunWokerAsync时触发的事件;

   
ProgressChanged:调用ReportProgress时触发的事件,程序会在该事件中进行进度报告的更新;

    RunWorkerCompleted:当异步操作已完成、被取消或引发异常时被触发。

    这种方法已经很少用到了,所以这里就不详细介绍了。

 

4、TAP又是什么?

    前面介绍了.NET提供的两种异步编程模式,分别为.NET 1.0中的APM和.NET
2.0中的EAP。虽然这两种异步编程模式可以实现多数情况下的异步编程,但是它们在MSDN文档上都被标注为了不推荐使用的实现方式,因为在.NET
4.0中,微软又提供了更简单的异步编程实现方式–TAP,基于任务的异步模式。

   
该模式主要使用System.Threading.Tasks命名空间中的Task<T>类来实现异步编程,所以在采用TAP之前,首先要引入System.Threading.Tasks命名空间。

    基于任务的异步模式(TAP,Task-based Asynchronous
Pattern)只使用一个方法就能表示异步操作的开始和完成,而APM却需要Beginxxx和Endxxx两个方法分别表示开始和结束,EAP则要求具有以Async为后缀的方法和一个或多个事件。在基于任务的异步模式中,只需要一个以TaskAsync为后缀的方法,通过向该方法传入CancellationToken参数,我们就可以很好地完成异步编程了。而且,还可以通过IProgress<T>接口来实现进度报告的功能。总体来说,使用TAP会减少我们的工作量,是代码更加简洁。

1 Task task=new Task(()=>{.......});
2 task.Start();

 

5、让异步编程So easy——C# 5.0中的async和await

    虽然.NET 1.0和.NET 2.0和.NET
4.0都对异步编程做了很好的支持,微软也逐渐地使用异步编程变得简单,但微软觉得现有的工作还不够,它希望使异步编程的开发过程更为简化,所以在.NET
4.5中,微软又提出了async和await两个关键字来支持异步编程。

    这也是目前.NET
Framework中最简单的异步编程实现方式,因为使用这个两个关键字进行异步编程,思考方式和实现同步编程时的完全一样。

   
async和await关键字不会让调用方法运行在新线程中,而是将方法分割成多个片段(片段的界限出现在方法内部使用await关键字的位置处),并使其中一些片段可以异步运行。await关键字处的代码片段是在线程池线程上运行的,而整个方法的调用确实同步的。所以,使用此方式编程不用考虑跨线程访问UI控件的问题,从而大大降低了异步编程的出错率。

1、什么是异步编程?
异步编程就是把耗时的操作放进一个单独的线程中进行处理(该线程需要将…

你会说编程语言吗?Do you speak code?

人和人之间的沟通交流会用到自己地方的语言,譬如英文、中文、日文等。

而编程语言就是和电脑沟通的语言,学会编程语言,就是用电脑听得懂的语言,告诉它帮你做你想要做的事情。

图片 3

异步编程,c#异步编程

几个月前因为一个事情被diao了。起因是临近上线的时候项目后端统一了消息协议(.proto),然后要我前端也支持。我研究了一天,没走通,要么依赖项太多,要么一直报错,而且需要使用的对象兼容性有问题。当时心里有些急,也有几份抵触这种方案,于是在会上说出了我的想法:能不能友好的发发json,兼容性好也不需要什么第三方解析。结果自然是被否决了,理由是大厂出品的,怎么可能不能用呢,用屁股想想就知道?你为啥遇到问题就想着退缩呢。我无语凝噎。重要是给我强调了能编程与会编程是不一样的

一、线程

1.1 主线程

.net使用main()方法做为程序的入口点,当调用该方法,主线程被创建。

1.2 工作者线程

由主线程创建的线程被称为工作者线程,用于去执行某一项具体的任务。

1.3 前台线程

默认情况下Thread.Start()创建的线程都是前台线程,前台线程可以阻止应用程序的终结,即所有的前台线程执行完成之后CLR*才能关闭应用程序。

前台线程属于工作者线程

1.4 后台线程

后台线程不影响应用程序的终结,所有的前台线程执行完成之后,无论后台线程是否执行完成,应用程序都会关闭。

后台线程一般用于处理一些无关紧要的任务(比如邮箱每隔一段时间检查一遍邮箱等)。后台线程属于工作者线程。

图片 4

 1      static void Main(string[] args)
 2         {
 3             Console.WriteLine("主线程开始");
 4             Thread t1 = new Thread(Task1);
 5             t1.Start();
 6 
 7             Thread t2 = new Thread(Task2);
 8             t2.IsBackground = true;  //后台线程
 9             t2.Start();
10         }
11 
12         public static void Task1()
13         {
14             Thread.Sleep(1000);   
15             Console.WriteLine("前台线程开始");
16         }
17 
18         public static void Task2()
19         {
20             Thread.Sleep(3000);
21             Console.WriteLine("后台线程开始");
22         }

View Code

以上代码运行结果如下图所示,由此可以看出前台线程执行完成之后,应用程序关闭。未输出“后台线程开始”,后台线程未执行。

图片 5

图片 6

 1     static void Main(string[] args)
 2         {
 3             Console.WriteLine("主线程开始");
 4             Thread t1 = new Thread(Task1);
 5             t1.Start();
 6 
 7             Thread t2 = new Thread(Task2);
 8             //t2.IsBackground = true;  //后台线程
 9             t2.Start();
10         }
11 
12         public static void Task1()
13         {
14             Thread.Sleep(1000);   
15             Console.WriteLine("前台线程开始");
16         }
17 
18         public static void Task2()
19         {
20             Thread.Sleep(3000);
21             Console.WriteLine("后台线程开始");
22         }

View Code

以上代码运行结果如下图所示,两个线程都为前台线程,两个线程全部执行完成之后,应用程序关闭。

 图片 7

开完会情绪有点低落,回到座位上打开github
继续找方案,不一会儿居然找到了!啪啪打脸的感觉好酸爽。然后开始思索…

二、线程池

线程池是为突然大量爆发的线程设计的,通过有限的几个固定的线程为大量的操作服务,减少了线程创建和销毁的时间,从而提高效率。

ThreadPool使用与并发运行若干个任务且运行时间不长且互不干扰的场景。ThreadPool创建的线程为后台线程。

图片 8

 1 static void Main(string[] args)
 2         {
 3             Console.WriteLine("主线程开始!");
 4             //创建要执行的任务
 5             WaitCallback workItem = state => Console.WriteLine("当前线程Id为:" + Thread.CurrentThread.ManagedThreadId);
 6             //重复调用10次
 7             for (int i = 0; i < 10; i++)
 8             {
 9                 ThreadPool.QueueUserWorkItem(workItem);
10             }
11             Console.ReadLine();
12         }

View Code

以上代码运行结果如下图所示,由此可见并不是每次执行任务都创建一个新的线程,是在循环利用线程池中维护的线程。如果去掉最后一句, Console.ReadLine(),仅输出“主程序开始”就直接退回,可知线程池创建的线程为后台线程。

图片 9

 

 持续维护中…….

 

 

 

 

 

CLR:

线程同任务的区别

async await的使用,什么时候使用,作用是什么

 

一、线程 1.1 主线程
.net使用main()方法做为程序的入口点,当调用该方法,主线程被创建。 1.2
工作者线程 由主线程创…

是否抵触


一件事带有抵触情绪之后,那基本是做不好的。在找方案的时候,眼中只看到“NO”,而自动忽略了“YES”。解决方法在你眼前你可能都看不到。就像一个人不想做事就找借口,这些借口在他自己看来都是很合理的。要带有这种情绪,那不如先别开始,要么自我消化,要么拿更好的方案去说服别人。

只要有一种解释是对自己有利的,我们便不想去推敲和反驳,再漏洞百出的事情看上去也不无可能,而且只要一种解释是可能的,我们就认定是一定的,强大的情绪大脑会阻止理性大脑往深入的想。而对于自己不利的解释,我们或忽略,或者异常仔细的去推敲,抓住一个漏洞则相信自己推翻了所有的解释。—-《暗时间》

所以一件事情在自己着手做的时候,先整理好类似的情绪。很多时候摆在我们面前的方案很多,如果是你熟悉的领域当然好选择,利弊清晰,能快速判断。但不熟悉的时候,就要注意到每个方案所适用的环境和条件,要归类和总结,自己所遇到的问题本身已经提供了一些前提条件。用这些条件先过滤到一部分。而不是瞎猫抓死耗子一样一个个去碰。

另外一点,我们在尝试新的事物的时候,总是会遇到各种困难,不同的人在不同的碰壁次数之后会退出。用程序员的话来说,我们都在for循环,区别在于你是什么情况下break;的。有的人退出阈值高,这是能坚持的一类人,有的人退出阈值低。过早退出的原因在于对未来的不确定性,对投资时间最终无法收到回报的恐惧。其实在坚持坚持,再想一想,答案马上就出来了。

不要等待


编程工作中我们经常遇到需求不完整,接口文档没有,接口返回值错,误诸如此类
别人的因素,然后导致项目进度缓慢或者停滞。其实我想说,编程简直太轻松愉快了,因为还有人给你提供需求,提供设计稿,你实现就行了。实际生活中很多事情根本没有人会告诉你需求,比如自己装修,你需要定风格,然后选材料,按照流程喊施工队伍,考虑风格和成本,考虑材料合不合适,喜不喜欢,甲醛高不高等等。建材市场鱼龙混杂,装修师傅可能阳奉阴违。即使你监工他都敢用料不够,总有一款让你交点学费。
可能很忙的时候,父母又生病了…..
生活的事情更考验的人的协作和沟通能力、承压能力。没有谁告诉你截止日期和责任,但是你没做好,就是没有做好,不要说什么装修的人坑了你。工作的事情责任范围清晰,不属于你的锅你都可以甩的远远的。但如果团队内部都是这样的氛围,那又能成什么事。雪崩的时候,没有一片雪花觉得自己有责任,应该积极主动起来,因为等待耗费的也是自己的时间。

做完了还是做好了


按照需求和效果图,你可能很快就实现了所有功能和效果。但是自己测试了吗?可用性高吗?稳定吗?兼容性如何?功能做完往往只到了一个阶段,后面还需要自己做检验性工作。确保交出去的东西是ok的,达到预期的。有信心对别人说我做好了,让别人用的放心。而不是说一句我做完了,暗藏的坑让别人先踩。有时间还应该思考一下,代码是否可以更简洁?接口设计是否可以更简明?多个类似方案是否可以统一成产品?思考的价值总会为你省下未来的时间,特别是重复劳动的时间、遇到问题沟通的时间。

知识体系完善


你选择的方案,都是你所知道的方案。也就是说,你的知识范围,决定了你的处理能力范围。甚至于,有的知识你会,但关键的时候你未必想的出来。这个过程就像解题,自己苦思冥想不得,看到答案恍然大悟。为什么呢?你的大脑就像一个图书馆,知识的碎片就如书架上的书,你想用的时候,发现找不到地址了。

知识分两种,一种是我们通常所谓的知识,即领域知识。二是关于我们大脑吸收知识的机制的知识,后者不妨成为元知识。虽说这也是领域知识,但跟其他的领域知识不同的是,它指导我们学习其它所有的领域知识。

除了完善我们的领域知识,也许需要补充一下其他领域的,譬如心理学和思维方面的,当然最好是到生活中去解决问题。

小结:会编程是基于现有经验办事的能力,而能编程是对整个事情的解决能力。在学习一门技术的成本差不多的情况下,差别就来自于编程之外的能力。

相关文章