热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

mosquitto的TLS功能测试,客户端使用paho.mqtt.golang(附JAVA版客户端实现)

1、SSLTLS简介SSL(SecureSocketLayer)安全套接层,是网景公司提出的用于保证Server与client之间安全通信的一种协议,该协议位于TCPIP协议与各应

1、SSL/TLS简介

  SSL(SecureSocket Layer)安全套接层,是网景公司提出的用于保证Server与client之间安全通信的一种协议,该协议位于TCP/IP协议与各应用层协议之间,即SSL独立于各应用层协议,因此各应用层协议可以透明地调用SSL来保证自身传输的安全性。目前,SSL被大量应用于http的安全通信中,MQTT协议与http协议同样属于应用层协议,因此也可以像http协议一样使用ssl为自己的通信提供安全保证。

  SSL与TLS(Transport LayerSecurity Protocol)之间的关系:TLS(TransportLayer Security,传输层安全协议)是IETF(InternetEngineering Task Force,Internet工程任务组)制定的一种新的协议,它建立在SSL 3.0协议规范之上,是SSL 3.0的后续版本。在TLS与SSL3.0之间存在着显著的差别,主要是它们所支持的加密算法不同,所以TLS与SSL3.0不能互操作。

2、使用Openssl创建tls证书

  SSL在身份认证过程中需要有一个双方都信任的CA签发的证书,CA签发证书是需要收费的,但是在测试过程中,可以自己产生一个CA,然后用自己产生的CA签发证书,下面的mosquitto的ssl功能的测试过程就是采用这一方式,其过程如下:

步骤一:产生自己的CA

openssl req -new -x509 -days 36500 -extensions v3_ca -keyout ca.key -out ca.crt
openssl req -new -x509 -days 36500 -extensions v3_ca -keyout ca.key -out ca.pem

步骤二:产生服务端证书

openssl genrsa -des3 -out server.key 2048
openssl req -out server.csr -key server.key -new
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 36500

步骤三:产生客户端证书

openssl genrsa -out client-key.pem 2048
openssl req -out client.csr -key client-key.pem -new
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client-crt.pem -days 36500

经过上面8条命令后,即可生成所需的所有证书文件,其中:

客户端使用:ca.pem、client-crt.pem、client-key.pem

服务端使用:ca.crt、server.crt、server.key

3、mosquitto.conf配置如下:

mosquitto的TLS功能测试,客户端使用paho.mqtt.golang(附JAVA版客户端实现)

4、golang客户端测试代码

  1 package cmd
  2 
  3 import (
  4     "crypto/tls"
  5     "crypto/x509"
  6     fmt "fmt"
  7     "io/ioutil"
  8     "os"
  9     "time"
 10 
 11     "github.com/apex/log"
 12     MQTT "github.com/eclipse/paho.mqtt.golang"
 13 )
 14 
 15 var ctx log.Interface
 16 
 17 const QoS = 0x02
 18 
 19 func init() {
 20     fmt.Printf("init mqtt test\n")
 21 
 22 }
 23 
 24 func RunMqttClient() {
 25     fmt.Printf("Run mqtt test\n")
 26     var logLevel = log.InfoLevel
 27     ctx = &log.Logger{
 28         Level:   logLevel,
 29         Handler: NewLogHanler(os.Stdout),
 30     }
 31 
 32     mqttClient := NewClient(
 33         ctx,
 34         "ttnhdl",
 35         "",
 36         "",
 37         fmt.Sprintf("ssl://%s", "192.168.195.201:8883"),
 38     )
 39 
 40     var err = mqttClient.Connect()
 41     if err != nil {
 42         ctx.WithError(err).Fatal("Could not connect to MQTT")
 43         fmt.Printf("Could not connect to MQTT\n")
 44     } else {
 45         fmt.Printf("Success connect to MQTT\n")
 46     }
 47 
 48     mqttClient.PublishUplink("test", "hello mqtt!")
 49     mqttClient.SubscribeUplink("test")
 50 
 51     for true {
 52 
 53     }
 54 }
 55 
 56 // Client connects to the MQTT server and can publish/subscribe on uplink, downlink and activations from devices
 57 type Client interface {
 58     Connect() error
 59     Disconnect()
 60 
 61     IsConnected() bool
 62 
 63     // Uplink pub/sub
 64     PublishUplink(topic string, msg string) Token
 65     SubscribeUplink(topic string) Token
 66 }
 67 
 68 type Token interface {
 69     Wait() bool
 70     WaitTimeout(time.Duration) bool
 71     Error() error
 72 }
 73 
 74 type simpleToken struct {
 75     err error
 76 }
 77 
 78 // Wait always returns true
 79 func (t *simpleToken) Wait() bool {
 80     return true
 81 }
 82 
 83 // WaitTimeout always returns true
 84 func (t *simpleToken) WaitTimeout(_ time.Duration) bool {
 85     return true
 86 }
 87 
 88 // Error contains the error if present
 89 func (t *simpleToken) Error() error {
 90     return t.err
 91 }
 92 
 93 type defaultClient struct {
 94     mqtt MQTT.Client
 95     ctx  log.Interface
 96 }
 97 
 98 func NewClient(ctx log.Interface, id, username, password string, brokers ...string) Client {
 99     tlsconfig := NewTLSConfig()
100 
101     mqttOpts := MQTT.NewClientOptions()
102 
103     for _, broker := range brokers {
104         mqttOpts.AddBroker(broker)
105     }
106 
107     mqttOpts.SetClientID("ypf_dewqfvcdeqfcdqwcdq")
108     mqttOpts.SetUsername(username)
109     mqttOpts.SetPassword(password)
110 
111     // TODO: Some tuning of these values probably won't hurt:
112     mqttOpts.SetKeepAlive(30 * time.Second)
113     mqttOpts.SetPingTimeout(10 * time.Second)
114 
115     // Usually this setting should not be used together with random ClientIDs, but
116     // we configured The Things Network's MQTT servers to handle this correctly.
117     mqttOpts.SetCleanSession(false)
118 
119     mqttOpts.SetDefaultPublishHandler(func(client MQTT.Client, msg MQTT.Message) {
120         ctx.WithField("message", msg).Warn("Received unhandled message")
121     })
122 
123     mqttOpts.SetConnectionLostHandler(func(client MQTT.Client, err error) {
124         ctx.WithError(err).Warn("Disconnected, reconnecting...")
125     })
126 
127     mqttOpts.SetOnConnectHandler(func(client MQTT.Client) {
128         ctx.Debug("Connected")
129     })
130 
131     mqttOpts.SetTLSConfig(tlsconfig)
132 
133     return &defaultClient{
134         mqtt: MQTT.NewClient(mqttOpts),
135         ctx:  ctx,
136     }
137 }
138 
139 var (
140     // ConnectRetries says how many times the client should retry a failed connection
141     COnnectRetries= 10
142     // ConnectRetryDelay says how long the client should wait between retries
143     COnnectRetryDelay= time.Second
144 )
145 
146 func (c *defaultClient) Connect() error {
147     if c.mqtt.IsConnected() {
148         return nil
149     }
150     var err error
151     for retries := 0; retries  {
152         token := c.mqtt.Connect()
153         token.Wait()
154         err = token.Error()
155         if err == nil {
156             break
157         }
158         <-time.After(ConnectRetryDelay)
159     }
160     if err != nil {
161         return fmt.Errorf("Could not connect: %s", err)
162     }
163     return nil
164 }
165 
166 func (c *defaultClient) Disconnect() {
167     if !c.mqtt.IsConnected() {
168         return
169     }
170     c.mqtt.Disconnect(25)
171 }
172 
173 func (c *defaultClient) IsConnected() bool {
174     return c.mqtt.IsConnected()
175 }
176 
177 func (c *defaultClient) PublishUplink(topic string, msg string) Token {
178     return c.mqtt.Publish(topic, QoS, false, msg)
179 }
180 
181 func (c *defaultClient) SubscribeUplink(topic string) Token {
182     return c.mqtt.Subscribe(topic, QoS, func(mqtt MQTT.Client, msg MQTT.Message) {
183         // Determine the actual topic
184         fmt.Printf("Success SubscribeUplink with msg:%s\n", msg.Payload())
185     })
186 }
187 
188 func NewTLSConfig() *tls.Config {
189     // Import trusted certificates from CAfile.pem.
190     // Alternatively, manually add CA certificates to
191     // default openssl CA bundle.
192     certpool := x509.NewCertPool()
193     pemCerts, err := ioutil.ReadFile("samplecerts/ca.pem")
194     if err == nil {
195         certpool.AppendCertsFromPEM(pemCerts)
196     }
197     fmt.Println("0. resd pemCerts Success")
198 
199     // Import client certificate/key pair
200     cert, err := tls.LoadX509KeyPair("samplecerts/client-crt.pem", "samplecerts/client-key.pem")
201     if err != nil {
202         panic(err)
203     }
204     fmt.Println("1. resd cert Success")
205 
206     // Just to print out the client certificate..
207     cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
208     if err != nil {
209         panic(err)
210     }
211     fmt.Println("2. resd cert.Leaf Success")
212 
213     // Create tls.Config with desired tls properties
214     return &tls.Config{
215         // RootCAs = certs used to verify server cert.
216         RootCAs: certpool,
217         // ClientAuth = whether to request cert from server.
218         // Since the server is set up for SSL, this happens
219         // anyways.
220         ClientAuth: tls.NoClientCert,
221         // ClientCAs = certs used to validate client cert.
222         ClientCAs: nil,
223         // InsecureSkipVerify = verify that cert contents
224         // match server. IP matches what is in cert etc.
225         InsecureSkipVerify: true,
226         // Certificates = list of certs client sends to server.
227         Certificates: []tls.Certificate{cert},
228     }
229 }

 5、测试效果

服务端启动:

mosquitto的TLS功能测试,客户端使用paho.mqtt.golang(附JAVA版客户端实现)

客户端运行:

mosquitto的TLS功能测试,客户端使用paho.mqtt.golang(附JAVA版客户端实现)

 

6、JAVA版客户端实现

依赖:org.eclipse.paho.client.mqttv3、bcprov-jdk16-1.45.jar

MqttServiceClient代码:

  1 package com.ypf.main;
  2 
  3 import java.util.Properties;
  4 
  5 import org.eclipse.paho.client.mqttv3.MqttCallback;
  6 import org.eclipse.paho.client.mqttv3.MqttClient;
  7 import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
  8 import org.eclipse.paho.client.mqttv3.MqttDeliveryToken;
  9 import org.eclipse.paho.client.mqttv3.MqttException;
 10 import org.eclipse.paho.client.mqttv3.MqttMessage;
 11 import org.eclipse.paho.client.mqttv3.MqttSecurityException;
 12 import org.eclipse.paho.client.mqttv3.MqttTopic;
 13 import org.eclipse.paho.client.mqttv3.internal.MemoryPersistence;
 14 
 15 import com.ypf.mqtt.SslUtil;
 16 
 17 /** 
 18  * 
 19  * @author LP by 2014-04-24
 20  *
 21  */
 22 public class MqttServiceClient implements MqttCallback {
 23 
 24     private static final String MQTT_HOST = "ssl://192.168.195.201:8884";
 25     private static final String MQTT_CLIENT = "Test_";
 26     public static String caFilePath = "D:/for-iot/LURA/src/mytest/samplecerts/ca.crt";
 27     public static String clientCrtFilePath = "D:/for-iot/LURA/src/mytest/samplecerts/client.crt";
 28     public static String clientKeyFilePath = "D:/for-iot/LURA/src/mytest/samplecerts/client.key";
 29     
 30     public static MqttServiceClient mqttServiceClient = null;
 31     
 32     private MqttClient client = null;
 33     private MqttConnectOptions optiOns= null;
 34     
 35     /**
 36      * 单例模式构造类
 37      */
 38     public static MqttServiceClient getInstance() {
 39         if (mqttServiceClient == null) {
 40             mqttServiceClient = new MqttServiceClient();
 41         }
 42         return mqttServiceClient;
 43     }
 44 
 45     private MqttServiceClient() {
 46         System.out.println("init MQTTClientService");
 47         init();
 48     }
 49     // The major API implementation follows :-
 50 
 51     /**
 52      * 初始化
 53      */
 54     private void init() {
 55         try {
 56         
 57             // host为主机名,test为clientid即连接MQTT的客户端ID,一般以客户端唯一标识符表示,MemoryPersistence设置clientid的保存形式,默认为以内存保存
 58             client = new MqttClient(MQTT_HOST, MQTT_CLIENT, new MemoryPersistence());
 59             // MQTT的连接设置
 60             optiOns= new MqttConnectOptions();
 61             // 设置是否清空session,这里如果设置为false表示服务器会保留客户端的连接记录,这里设置为true表示每次连接到服务器都以新的身份连接
 62             options.setCleanSession(true);
 63             // 设置连接的用户名
 64             options.setUserName("ypf");
 65             // 设置连接的密码
 66             options.setPassword("ruijie".toCharArray());
 67             // 设置超时时间 单位为秒
 68             options.setConnectionTimeout(50);
 69             // 设置会话心跳时间 单位为秒 服务器会每隔1.5*20秒的时间向客户端发送个消息判断客户端是否在线,但这个方法并没有重连的机制
 70             options.setKeepAliveInterval(30);
 71             // TLS连接配置
 72             options.setSocketFactory(
 73                     SslUtil.getSocketFactory(caFilePath, clientCrtFilePath, clientKeyFilePath, "cs123456"));
 74 
 75             // 设置回调
 76             client.setCallback(this);
 77             
 78         } catch (Exception e) {
 79             e.printStackTrace();
 80         }
 81     }
 82     /**
 83      * 连接到MQTT
 84      */
 85     void connect() {
 86         System.out.println("Start connect----------");
 87         try {
 88             client.connect(options);
 89             //订阅主题的方法,2为消息的质量
 90             client.subscribe("+/#", 2);
 91             //发送消息
 92             publish("test", "撒打发水电费水电费");
 93         } catch (Exception e) {
 94             e.printStackTrace();
 95         }
 96     }
 97     
 98     /**
 99      * 断开连接到MQTT
100      */
101     public void disconnect() {
102         System.out.println("Start disconnect----------");
103         try {
104             client.disconnect();
105         } catch (MqttSecurityException e) {
106             e.printStackTrace();
107         } catch (MqttException e) {
108             e.printStackTrace();
109         }
110     }
111 
112     /** 
113      * 发布消息
114      * @param topic 主题
115      * @param msg 消息
116      */
117     public void publish(String topic, String msg) {
118         System.out.println("Start publish----------");
119         try {
120             MqttTopic mqttTopic = client.getTopic(topic);
121             //2为消息的质量
122             MqttDeliveryToken messageToken = mqttTopic.publish(msg.getBytes(), 2, true);
123             System.out.println("publish success==>"+messageToken.getMessage());
124 //            client.publish(topic, 2, msg);
125         } catch (Exception e) {
126             e.printStackTrace();
127         }
128     }
129     
130     
131 // -------------------------------------------------回调方法------------------------------------------------------------//
132     
133     /** 
134      * 连接断开触发此方法
135      */
136     @Override
137     public void connectionLost(Throwable cause) {
138         System.out.println("Connection Lost---------->" + cause.getMessage());
139     }
140 
141     /** 
142      * 消息达到触发此方法
143      */
144     @Override
145     public void messageArrived(MqttTopic topic, MqttMessage message)
146             throws Exception {
147         System.out.println(topic + ":" + message.toString());
148     }
149 
150     /**
151      * 消息发送成功触发此方法
152      */
153     @Override
154     public void deliveryComplete(MqttDeliveryToken token)  {
155         try {
156             System.out.println("deliveryComplete---------" + token.getMessage());
157         } catch (MqttException e) {
158             e.printStackTrace();
159         }
160     }
161 
162     
163     public static void main(String[] args)throws Exception {
164         
165         //MqttServiceClient.getInstance().disconnect();
166         MqttServiceClient.getInstance().connect();
167         
168         new Thread() {
169             public void run() {
170                 int count = 0;
171                 while(true && count <3) {
172                     try {
173                         Thread.sleep(1000*3);
174                     } catch (InterruptedException e) {
175                         e.printStackTrace();
176                     }
177                     MqttServiceClient.getInstance().publish("test1/ypf", "hello world ! count=" + count);
178                     count ++;
179                 }
180             };
181         }.start();
182     }
183     
184 }

SslUtil代码:
 1 package com.ypf.mqtt;
 2 
 3 import java.io.ByteArrayInputStream;
 4 import java.io.InputStreamReader;
 5 import java.nio.file.Files;
 6 import java.nio.file.Paths;
 7 import java.security.KeyPair;
 8 import java.security.KeyStore;
 9 import java.security.Security;
10 import java.security.cert.X509Certificate;
11 
12 import javax.net.ssl.KeyManagerFactory;
13 import javax.net.ssl.SSLContext;
14 import javax.net.ssl.SSLSocketFactory;
15 import javax.net.ssl.TrustManagerFactory;
16 
17 import org.bouncycastle.jce.provider.BouncyCastleProvider;
18 import org.bouncycastle.openssl.*;
19 
20 public class SslUtil {
21     public static SSLSocketFactory getSocketFactory(final String caCrtFile, final String crtFile, final String keyFile,
22             final String password) throws Exception {
23         Security.addProvider(new BouncyCastleProvider());
24 
25         // load CA certificate
26         PEMReader reader = new PEMReader(
27                 new InputStreamReader(new ByteArrayInputStream(Files.readAllBytes(Paths.get(caCrtFile)))));
28         X509Certificate caCert = (X509Certificate) reader.readObject();
29         reader.close();
30 
31         // load client certificate
32         reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(Files.readAllBytes(Paths.get(crtFile)))));
33         X509Certificate cert = (X509Certificate) reader.readObject();
34         reader.close();
35 
36         // load client private key
37         reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(Files.readAllBytes(Paths.get(keyFile)))),
38                 new PasswordFinder() {
39                     @Override
40                     public char[] getPassword() {
41                         return password.toCharArray();
42                     }
43                 });
44         KeyPair key = (KeyPair) reader.readObject();
45         reader.close();
46 
47         // CA certificate is used to authenticate server
48         KeyStore caKs = KeyStore.getInstance(KeyStore.getDefaultType());
49         caKs.load(null, null);
50         caKs.setCertificateEntry("ca-certificate", caCert);
51         TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
52         tmf.init(caKs);
53 
54         // client key and certificates are sent to server so it can authenticate
55         // us
56         KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
57         ks.load(null, null);
58         ks.setCertificateEntry("certificate", cert);
59         ks.setKeyEntry("private-key", key.getPrivate(), password.toCharArray(),
60                 new java.security.cert.Certificate[] { cert });
61         KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
62         kmf.init(ks, password.toCharArray());
63 
64         // finally, create SSL socket factory
65         SSLContext cOntext= SSLContext.getInstance("TLSv1");
66         context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
67 
68         return context.getSocketFactory();
69     }
70 }

 


推荐阅读
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • Oracle分析函数first_value()和last_value()的用法及原理
    本文介绍了Oracle分析函数first_value()和last_value()的用法和原理,以及在查询销售记录日期和部门中的应用。通过示例和解释,详细说明了first_value()和last_value()的功能和不同之处。同时,对于last_value()的结果出现不一样的情况进行了解释,并提供了理解last_value()默认统计范围的方法。该文对于使用Oracle分析函数的开发人员和数据库管理员具有参考价值。 ... [详细]
  • 本文介绍了南邮ctf-web的writeup,包括签到题和md5 collision。在CTF比赛和渗透测试中,可以通过查看源代码、代码注释、页面隐藏元素、超链接和HTTP响应头部来寻找flag或提示信息。利用PHP弱类型,可以发现md5('QNKCDZO')='0e830400451993494058024219903391'和md5('240610708')='0e462097431906509019562988736854'。 ... [详细]
  • 本文介绍了Windows操作系统的版本及其特点,包括Windows 7系统的6个版本:Starter、Home Basic、Home Premium、Professional、Enterprise、Ultimate。Windows操作系统是微软公司研发的一套操作系统,具有人机操作性优异、支持的应用软件较多、对硬件支持良好等优点。Windows 7 Starter是功能最少的版本,缺乏Aero特效功能,没有64位支持,最初设计不能同时运行三个以上应用程序。 ... [详细]
  • 本文介绍了如何使用PHP向系统日历中添加事件的方法,通过使用PHP技术可以实现自动添加事件的功能,从而实现全局通知系统和迅速记录工具的自动化。同时还提到了系统exchange自带的日历具有同步感的特点,以及使用web技术实现自动添加事件的优势。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • CentOS 7部署KVM虚拟化环境之一架构介绍
    本文介绍了CentOS 7部署KVM虚拟化环境的架构,详细解释了虚拟化技术的概念和原理,包括全虚拟化和半虚拟化。同时介绍了虚拟机的概念和虚拟化软件的作用。 ... [详细]
  • Google在I/O开发者大会详细介绍Android N系统的更新和安全性提升
    Google在2016年的I/O开发者大会上详细介绍了Android N系统的更新和安全性提升。Android N系统在安全方面支持无缝升级更新和修补漏洞,引入了基于文件的数据加密系统和移动版本的Chrome浏览器可以识别恶意网站等新的安全机制。在性能方面,Android N内置了先进的图形处理系统Vulkan,加入了JIT编译器以提高安装效率和减少应用程序的占用空间。此外,Android N还具有自动关闭长时间未使用的后台应用程序来释放系统资源的机制。 ... [详细]
  • 如何提高PHP编程技能及推荐高级教程
    本文介绍了如何提高PHP编程技能的方法,推荐了一些高级教程。学习任何一种编程语言都需要长期的坚持和不懈的努力,本文提醒读者要有足够的耐心和时间投入。通过实践操作学习,可以更好地理解和掌握PHP语言的特异性,特别是单引号和双引号的用法。同时,本文也指出了只走马观花看整体而不深入学习的学习方式无法真正掌握这门语言,建议读者要从整体来考虑局部,培养大局观。最后,本文提醒读者完成一个像模像样的网站需要付出更多的努力和实践。 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
author-avatar
小伙砸
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有