目录
Go语言网络协议基础
协议
实现
跨平台网络抽象
简单代码展示
服务端
客户端
服务端客户端通信实战
Go Linux服务端
Go Linux客户端
Windows C++ 客户端
总结
Go语言网络协议基础
在 Go 语言中,net/http
包提供了强大的工具来创建 HTTP 服务器。以下是创建基本服务器的步骤:
net/http
包是 Go 语言用于网络编程,特别是用于构建和处理 HTTP 和 HTTPS 协议的应用程序的标准库。我们来探讨一下它的实现方式和它所遵循的协议。
协议
-
HTTP(超文本传输协议):
net/http
包最主要的功能是支持 HTTP,这是一种应用层协议,用于分布式、协作性和超媒体信息系统。HTTP 是一个无状态的请求-响应协议,通常运行在 TCP/IP 协议之上。 -
HTTPS(安全的 HTTP):HTTPS 是 HTTP 的安全版本,它在传输层使用 SSL/TLS 协议来提供加密通信和安全的身份认证。
net/http
包通过内置的crypto/tls
包支持 HTTPS。
实现
-
HTTP 服务器:
net/http
包通过提供http.ListenAndServe
函数来启动 HTTP 服务器。这个函数内部创建了一个net.Listener
,通常是一个 TCP 监听器,用于监听传入的 HTTP 请求。- 当接收到 HTTP 请求时,它将请求分派给注册的处理函数(使用
http.HandleFunc
或http.Handle
注册)。 - 这些处理函数接收
http.ResponseWriter
和http.Request
对象,用于构造响应和解析请求。
-
HTTP 客户端:
net/http
包提供了一个默认的客户端(http.DefaultClient
),该客户端使用http.Transport
来管理 HTTP 请求的底层细节。http.Transport
管理连接池,处理请求的发送,以及接收响应。- 它通过 TCP 连接发送 HTTP 请求,并接收响应。对于 HTTPS,它还负责处理 TLS 握手过程。
-
Request 和 Response 处理:
- HTTP 请求和响应都被抽象为
http.Request
和http.Response
类型。 - 这些类型提供了丰富的方法和字段,用于访问和修改 HTTP 请求和响应的各个部分,如 URL、头部、主体等。
- HTTP 请求和响应都被抽象为
-
扩展性和灵活性:
net/http
包设计灵活,易于扩展。开发者可以自定义处理函数、中间件、客户端的行为等。- 它还允许开发者替换或增强底层的传输机制,例如通过实现自定义的
http.RoundTripper
。
跨平台网络抽象
-
网络 I/O:
net/http
包的底层网络 I/O 操作(如 TCP 连接)主要依赖于 Go 语言的net
包。net
包提供了一个平台独立的接口来处理网络 I/O 操作。 -
Go 调度器:Go 的运行时包括一个高效的调度器,用于调度 Go 程程(goroutines)。这个调度器是独立于操作系统线程的,但会与之交互,以高效地利用多核心处理器。网络 I/O 操作通常在 Go 程程中异步执行。
简单代码展示
服务端
import ("net/http"
)//创建处理函数
func handler(w http.ResponseWriter, r *http.Request) {w.Write([]byte("Hello, 世界"))
}//注册处理函数
http.HandleFunc("/", handler)//启动
http.ListenAndServe(":8080", nil)
客户端
//发送请求
resp, err := http.Get("http://localhost:8080")
if err != nil {// 处理错误
}
defer resp.Body.Close()//读取响应
body, err := ioutil.ReadAll(resp.Body)
if err != nil {// 处理错误
}
fmt.Println(string(body))
服务端客户端通信实战
Go Linux服务端
package mainimport ("encoding/json""fmt""io/ioutil""log""net/http""strings"
)// Message 结构体用于 JSON 响应和请求
type Message struct {Text string `json:"text"`
}func main() {http.HandleFunc("/echo", echoHandler) // 处理 /echo 路径http.HandleFunc("/post", postHandler) // 处理 /post 路径fmt.Println("服务器启动在 http://localhost:8080")log.Fatal(http.ListenAndServe(":8080", nil)) // 启动服务器
}// echoHandler 用于回应客户端发送的消息
func echoHandler(w http.ResponseWriter, r *http.Request) {// 只接受 GET 请求if r.Method != http.MethodGet {http.Error(w, "只支持 GET 请求", http.StatusMethodNotAllowed)return}message := r.URL.Query().Get("message")response := Message{Text: message}jsonResponse, err := json.Marshal(response)if err != nil {http.Error(w, err.Error(), http.StatusInternalServerError)return}w.Header().Set("Content-Type", "application/json")w.Write(jsonResponse)
}// postHandler 接受 JSON 数据并返回
func postHandler(w http.ResponseWriter, r *http.Request) {// 只接受 POST 请求if r.Method != http.MethodPost {http.Error(w, "只支持 POST 请求", http.StatusMethodNotAllowed)fmt.Println("ERROR 1")return}var message Messagebody, err := ioutil.ReadAll(r.Body)if err != nil {http.Error(w, "无法读取 body", http.StatusBadRequest)fmt.Println("ERROR 2")return}defer r.Body.Close()var cleanedJSON stringjsonString := string(body)if !isValidJSON(jsonString) {cleanedJSON = removeInvalidChars(jsonString)}err = json.Unmarshal([]byte(cleanedJSON), &message)if err != nil {http.Error(w, "无法解析 JSON", http.StatusBadRequest)fmt.Println("ERROR 3")return}jsonResponse, err := json.Marshal(message)if err != nil {http.Error(w, err.Error(), http.StatusInternalServerError)fmt.Println("ERROR 4")return}w.Header().Set("Content-Type", "application/json")w.Write(jsonResponse)
}func isValidJSON(jsonString string) bool {// 检查JSON字符串是否包含特殊字符return strings.IndexFunc(jsonString, func(r rune) bool {return r < 32 || r >= 127}) == -1
}func removeInvalidChars(jsonString string) string {var validChars []runefor _, r := range jsonString {if r >= 32 && r < 127 {validChars = append(validChars, r)}}return string(validChars)
}
解释一下
http.HandleFunc
的工作原理:
函数签名:
http.HandleFunc
需要两个参数:一个字符串(表示 URL 路径)和一个处理函数。这个处理函数必须符合特定的签名:它接受一个http.ResponseWriter
和一个*http.Request
作为参数。函数引用:在
http.HandleFunc("/echo", echoHandler)
中,echoHandler
是一个函数引用,而不是一个函数调用。这意味着我们传递的是函数本身,而不是执行该函数的结果。延迟执行:当服务器运行并接收到路径为
/echo
的 HTTP 请求时,net/http
包的内部机制会调用echoHandler
函数,并且为它提供必要的http.ResponseWriter
和*http.Request
参数。这是在请求发生时发生的,而不是在设置路由时。回调机制:可以把
echoHandler
理解为一个回调函数。在编程中,回调函数是在特定事件或条件满足时由另一个函数调用的函数。在这种情况下,事件是对/echo
路径的 HTTP 请求。因此,当你在
http.HandleFunc
中看到echoHandler
没有传递参数,这是因为你只是在注册一个当特定 HTTP 请求到来时应该被调用的函数,而实际的参数传递是在请求处理时由net/http
包自动处理的。
这里我们加入json解析的可能错误处理,处理来自windows客户端的json格式错误问题。然后让windows客户端来访问Linux上go写的服务。
Go Linux客户端
需要配置好服务端设定的IP、端口、访问接口等。还有向URL发字符串时特殊字符例如空格、&、¥、%这些如何处理的问题。
package mainimport ("bytes""encoding/json""fmt""io/ioutil""net/http"
)type Message struct {Text string `json:"text"`
}func main() {getResponse, err := http.Get("http://localhost:8080/echo?message=Hello%2C%20Go%21")if err != nil {panic(err)}defer getResponse.Body.Close()body, err := ioutil.ReadAll(getResponse.Body)if err != nil {panic(err)}fmt.Println("GET Response:", string(body))message := Message{Text: "Hi from Client"}jsonRequest, err := json.Marshal(message)if err != nil {panic(err)}postResponse, err := http.Post("http://localhost:8080/post", "application/json", bytes.NewBuffer(jsonRequest))if err != nil {panic(err)}defer postResponse.Body.Close()body, err = ioutil.ReadAll(postResponse.Body)if err != nil {panic(err)}fmt.Println("POST Response:", string(body))
}
这里打一个Get请求和一个Post请求,经测试,Linux本地没问题。
Windows C++ 客户端
windows客户端开100线程取while true的访问,看看服务器能否顶住。
#include <windows.h>
#include <vector>
#include <winhttp.h>
#include <iostream>
#include <string>
#include <sstream>
#include <thread>#pragma comment(lib, "winhttp.lib")std::string SendRequest(const std::string& serverName, const std::string& apiPath, bool isPost, const std::string& postData = "") {std::stringstream responseStream;HINTERNET hSession = WinHttpOpen(L"A WinHTTP Example Program/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);if (!hSession) {responseStream << "WinHttpOpen failed with error: " << GetLastError();return responseStream.str();}std::wstring wServerName = std::wstring(serverName.begin(), serverName.end());std::wstring wApiPath = std::wstring(apiPath.begin(), apiPath.end());HINTERNET hConnect = WinHttpConnect(hSession, wServerName.c_str(), 8080, 0);if (!hConnect) {responseStream << "WinHttpConnect failed with error: " << GetLastError();WinHttpCloseHandle(hSession);return responseStream.str();}LPCWSTR method = isPost ? L"POST" : L"GET";HINTERNET hRequest = WinHttpOpenRequest(hConnect, method, wApiPath.c_str(), NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0);if (!hRequest) {responseStream << "WinHttpOpenRequest failed with error: " << GetLastError();WinHttpCloseHandle(hConnect);WinHttpCloseHandle(hSession);return responseStream.str();}BOOL bResults = FALSE;std::wstring wPostData = std::wstring(postData.begin(), postData.end());if (isPost) {bResults = WinHttpSendRequest(hRequest, L"Content-Type: application/json", -1, (LPVOID)wPostData.c_str(), wPostData.size() * sizeof(wchar_t), wPostData.size() * sizeof(wchar_t), 0);}else {bResults = WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0);}if (!bResults) {responseStream << "WinHttpSendRequest failed with error: " << GetLastError();}else {bResults = WinHttpReceiveResponse(hRequest, NULL);DWORD dwSize = 0;DWORD dwDownloaded = 0;LPSTR pszOutBuffer;if (bResults) {do {dwSize = 0;if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) {responseStream << "WinHttpQueryDataAvailable failed with error: " << GetLastError();break;}pszOutBuffer = new char[dwSize + 1];if (!pszOutBuffer) {responseStream << "Out of memory";dwSize = 0;break;}else {ZeroMemory(pszOutBuffer, dwSize + 1);if (!WinHttpReadData(hRequest, (LPVOID)pszOutBuffer, dwSize, &dwDownloaded)) {responseStream << "WinHttpReadData failed with error: " << GetLastError();}else {responseStream.write(pszOutBuffer, dwDownloaded);}delete[] pszOutBuffer;}} while (dwSize > 0);}}if (hRequest) WinHttpCloseHandle(hRequest);if (hConnect) WinHttpCloseHandle(hConnect);if (hSession) WinHttpCloseHandle(hSession);return responseStream.str();
}std::string serverName = "192.168.125.104";
std::string getApiPath = "/echo?message=Hello%2C%20Go%21";
std::string postApiPath = "/post";
std::string postData = "{\"text\":\"HifromClient\"}";void myfunc_work() {while (1){std::string getResponse = SendRequest(serverName, getApiPath, false);std::string postResponse = SendRequest(serverName, postApiPath, true, postData);std::cout << "GET Response: " << getResponse << std::endl;std::cout << "POST Response: " << postResponse << std::endl;}
}int main() {std::vector<std::thread> thvec;for (int i = 0; i < 100; i++){thvec.push_back(std::thread(myfunc_work));}for (int i = 0; i < 10; i++){thvec[i].join();}return 0;
}
实际上肯定不是100个线程同时访问,我的服务器时intel N100的低功耗芯片,Go语言编写的服务能顶住。没有错误出现,都返回成功了 32G的服务器服务内存占比40左右:
多线程访问图:
Linux服务器cpu占用:
总结
简单的服务端与客户端通信,检验了go语言高并发的恐怖。