目录
Go语言网络协议基础
协议
实现
跨平台网络抽象
简单代码展示
服务端
客户端
服务端客户端通信实战
Go Linux服务端
Go Linux客户端
Windows C++ 客户端
总结
在 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.HandleFunc
或 http.Handle
注册)。http.ResponseWriter
和 http.Request
对象,用于构造响应和解析请求。HTTP 客户端:
net/http
包提供了一个默认的客户端(http.DefaultClient
),该客户端使用 http.Transport
来管理 HTTP 请求的底层细节。http.Transport
管理连接池,处理请求的发送,以及接收响应。Request 和 Response 处理:
http.Request
和 http.Response
类型。扩展性和灵活性:
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))
package main
import (
"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 Message
body, 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 string
jsonString := 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 []rune
for _, 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写的服务。
需要配置好服务端设定的IP、端口、访问接口等。还有向URL发字符串时特殊字符例如空格、&、¥、%这些如何处理的问题。
package main
import (
"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客户端开100线程取while true的访问,看看服务器能否顶住。
#include
#include
#include
#include
#include
#include
#include
#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 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语言高并发的恐怖。