最近因工作需要为外部公司提供http服务,由于内容比较少,同时为了方便安装,就想着自己写一个简单的服务器。
思路是将一个Http服务器嵌入一个Windows Service程序中,于是在网上找了很多资料和源码。C#中实现http服务一般使用Socket或TcpListener两种,本文使用的是Socket方式。
一、HTTP报文格式
Post报文:
POST /Adapter.KeyInitialization HTTP/1.1
Content-Length: 191
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Host: 115.156.249.117:11250
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.5.1 (Java/1.7.0_45)
Accept-Encoding: gzip,deflate
postdata=%7B%22key_extra_info%22%3A%22zehge%22%2C%22key_name%22%3A%22%E9%92%A5%E5%8C%9935313032-5956-4331-3031-3039ffffffff%22%2C%22key_rfid%22%3A%2235313032-5956-4331-3031-3039ffffffff%22%7D
熟悉编码的童鞋会发现,上面postdata=之后的部分其实是Url编码,所以在解析时需要用到HttpUtility.UrlDecode(String urlEncodeStr);
【注意】post有两种传参数的方式
1、参数放在url中,Content-Length值为0。
请求报文:
POST http://192.168.80.194:8080/JBPlatform/DFESerUpLoadData.posttest?jsonData=tetet HTTP/1.1
Content-Type: application/x-www-form-urlencoded
cache-control: no-cache
Postman-Token: 202cd59d-b2e2-4370-a88c-8e7abaf7a8d9
User-Agent: PostmanRuntime/7.6.0
Accept: */*
Host: 192.168.80.194:8080
accept-encoding: gzip, deflate
content-length: 0
Connection: keep-alive
返回报文:
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 64
Date: Thu, 23 Apr 2020 03:46:50 GMT
{"data":"成功","jsonData":"tetet","result":1,"runStatus":null}
2、参数放在body中,Content-Length需根据body计算长度
请求报文:
POST http://192.168.80.194:8080/JBPlatform/DFESerUpLoadData.posttest HTTP/1.1
Content-Type: application/x-www-form-urlencoded
cache-control: no-cache
Postman-Token: be9877fa-bad6-49db-82cf-5a993faf2fc8
User-Agent: PostmanRuntime/7.6.0
Accept: */*
Host: 192.168.80.194:8080
accept-encoding: gzip, deflate
content-length: 15
Connection: keep-alive
jsonData=tetete
返回报文:
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 65
Date: Thu, 23 Apr 2020 03:31:36 GMT
{"data":"成功","jsonData":"tetete","result":1,"runStatus":null}
Get报文:
GET / HTTP/1.1
Content-Length: 191
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Host: 115.156.249.117:11250
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.5.1 (Java/1.7.0_45)
Accept-Encoding: gzip,deflate
二、简单HTTP服务器的实现
1、根据报文格式写一个解析报文的类
using System; using System.Collections.Generic; using System.IO; using System.Text; /// <summary> /// 描述:HTTP请求类,用于解析HTTP报文 /// 编写:gc /// 日期:2016/3/8 /// 版本:1.0.1.3 /// </summary> public class MyHTTPRequest { public string Method { get; private set; } public string Url { get; private set; } public string Protocol { get; private set; } private Dictionary<string, string> properties = null; public Dictionary<string, string> Properties { get { if (properties == null) properties = new Dictionary<string, string>(); return properties; } } public string Parameter { get; private set; } public MyHTTPRequest() { //Nothing to do! } public MyHTTPRequest(string requestString) { using (StringReader sr = new StringReader(requestString)) { var line = sr.ReadLine(); if (!IsNullOrWhiteSpace(line)) { string[] headerInfo = line.Split(new char[] { ' ' }); if (headerInfo.Length == 3) { SetHeader(headerInfo[0], headerInfo[1], headerInfo[2]); for (line = sr.ReadLine(); !IsNullOrWhiteSpace(line); line = sr.ReadLine()) { string[] tokens = line.Split(new string[] { ": " }, StringSplitOptions.None); if (tokens.Length == 2) { Properties.Add(tokens[0], tokens[1]); } else { //Console.WriteLine(line); //打印调试信息 } } switch (this.Method.ToUpper()) { case "GET": Parameter = string.Empty; break; case "POST": { try { string tempStr = sr.ReadToEnd().ToString().Trim(); SKMPInput input = new SKMPInput(tempStr); Parameter = input.InputParameter; //SKMPInput input = JSONConvertHelper.ToObject<SKMPInput>(tempStr); //Parameter = JSONConvertHelper.ToJSONString(input.postdata); } catch (Exception ex) { string tempStr = sr.ReadToEnd().ToString().Trim(); SKMPInput input = new SKMPInput(tempStr); Parameter = input.InputParameter; //SKMPInput input = JSONConvertHelper.ToObject<SKMPInput>(tempStr); //Parameter = JSONConvertHelper.ToJSONString(input.postdata); } //Parameter = sr.ReadToEnd().ToString().Trim(); } break; default: break; } } } } } public void SetHeader(string method, string url, string protocol) { this.Method = method; this.Url = url; this.Protocol = protocol; } public override string ToString() { StringBuilder sb = new StringBuilder(); sb.AppendFormat("Method:{0}<br/>", Method); sb.AppendFormat("Url:{0}<br/>", Url); sb.AppendFormat("Protocol:{0}<br/>", Protocol); foreach (var item in Properties) { sb.AppendFormat("{0}:{1}<br/>", item.Key, item.Value); } sb.AppendFormat("Parameter:{0}<br/>", Parameter); return sb.ToString(); } public static bool IsNullOrWhiteSpace(string value) { if (value != null) { for (int i = 0; i < value.Length; i++) { if (!char.IsWhiteSpace(value[i])) { return false; } } } return true; } }
2、服务器主体程序
using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System; using System.Diagnostics; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; /// <summary> /// 描述:HTTP服务器 /// 编写:gc /// 日期:2016/3/8 /// 版本:1.0.1.3 /// </summary> public class MyHTTPServer { private string companyName = "**公司"; private string localServerInfo = "MyHTTPServer/1.0"; private string localServerIP = "localhost"; private int localServerPort = 8080; private string localServerContentType = "image/jpeg, application/x-ms-application, image/gif, application/xaml+xml, image/pjpeg, application/x-ms-xbap, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*"; private Encoding localServerEncoding = Encoding.GetEncoding("gb2312"); private TicketService ticketService = null; private string targetHostIP; private int targetHostPort = 11250; private Thread m_Thread; //HTTP服务主线程 public bool IsAlive { get { return this.m_Thread.IsAlive; } } /// <summary> /// 监听标志位 /// </summary> private bool isRun = false; Socket serverSocket; public CookieContainer CookieContainer { get; set; } public MyHTTPServer() { Initialize(); } /// <summary> /// 初始化服务器参数 /// </summary> private void Initialize() { this.companyName = ServerGlobal.ServerGlobalInstance.CompanyName; this.localServerInfo = ServerGlobal.ServerGlobalInstance.LocalServerInfo; this.localServerIP = ServerGlobal.ServerGlobalInstance.LocalServerIP; this.localServerPort = ServerGlobal.ServerGlobalInstance.LocalServerPort; this.localServerContentType = ServerGlobal.ServerGlobalInstance.LocalServerContentType; this.localServerEncoding = ServerGlobal.ServerGlobalInstance.LocalServerEncoding; this.targetHostIP = ServerGlobal.ServerGlobalInstance.TargetHostIP; this.targetHostPort = ServerGlobal.ServerGlobalInstance.TargetHostPort; ticketService = TicketService.TicketServiceInstance; } /// <summary> /// 启动服务器 /// </summary> public void Start() { isRun = true; this.m_Thread = new Thread(new ThreadStart(this.Listen)); this.m_Thread.Start(); ticketService.Start(); } /// <summary> /// 停止服务器 /// </summary> public void Stop() { isRun = false; this.m_Thread.Abort(); serverSocket.Close();//释放Socket占用的端口号 ticketService.Stop(); } /// <summary> /// 监听 /// </summary> private void Listen() { try { IPEndPoint ipep = new IPEndPoint(IPAddress.Any, this.localServerPort); serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); serverSocket.Bind(ipep); serverSocket.Listen(10); while (isRun) { Socket clientSocket = serverSocket.Accept(); ThreadPool.QueueUserWorkItem(new WaitCallback(ReceiveData), clientSocket); } } catch (Exception ex) { MyServerRunLog.WriteRunLog("listening Error: " + ex.Message); } } /// <summary> /// 接收数据 /// </summary> /// <param name="socket">Socket</param> private void ReceiveData(object socket) { Socket clientSocket = socket as Socket; Byte[] buffer = new Byte[8192]; //Socket默认缓冲区为8K,当数据量大于8K时此处可能会产生Bug IPEndPoint clientep = (IPEndPoint)clientSocket.RemoteEndPoint; string clientMessage = string.Empty; try { clientSocket.Receive(buffer, 0, buffer.Length, SocketFlags.None); clientMessage = localServerEncoding.GetString(buffer); if (IsNullOrWhiteSpace(clientMessage)) { } byte[] data = WriteSuccessResponse(clientMessage); clientSocket.Send(data, data.Length, SocketFlags.None); } catch (Exception ex) { StringBuilder sb = new StringBuilder(); WriteFailure(sb); sb.AppendFormat("<html><head><title>Server throw an exception:{1}</title></head><body>{0}</body></html>", ex.ToString(), ex.GetType().FullName); var data = localServerEncoding.GetBytes(sb.ToString()); clientSocket.Send(data, data.Length, SocketFlags.None); } clientSocket.Shutdown(SocketShutdown.Both); } private byte[] WriteSuccessResponse(string clientMessage) { MyHTTPRequest request = new MyHTTPRequest(clientMessage); StringBuilder sb = new StringBuilder(); sb.AppendFormat("{0} 200 ok ", request.Protocol); if (null != request.Method) { switch (request.Method.ToUpper()) { case "GET": { sb.AppendLine("connection: close"); sb.AppendLine(); sb.AppendFormat("<html><head><title>Connect Sucess {1}</title></head><body><h2>Welcome to my http server</h2>{0}<h3><div><h3>Your requested content:</h3>{2}</div>Author: <a href='http://www.baidu.com/'>百度</a></h3></body></html>", request.ToString(), request.Url, clientMessage); return Encoding.GetEncoding("gb2312").GetBytes(sb.ToString()); // } break; case "POST": { string returnValue = DealWithUrl(request); StringBuilder contentBody = new StringBuilder(); contentBody = contentBody.Append(returnValue); //contentBody = contentBody.AppendFormat("<html><head><title>You request Url is {0}</title></head><body>{1}</body></html>", request.Url, returnValue); int contentLength = localServerEncoding.GetBytes(contentBody.ToString()).Length; sb.AppendLine(string.Format("Server: {0} ", localServerInfo)); sb.AppendLine(string.Format("Date: {0} ", DateTime.Now.GetDateTimeFormats('r')[0].ToString())); sb.AppendLine(string.Format("Content-Type: {0} ", localServerContentType)); sb.AppendLine(string.Format("Last-Modified: {0} ", DateTime.Now.GetDateTimeFormats('r')[0].ToString())); sb.AppendLine(string.Format("Content-Length: {0} ", contentLength)); sb.AppendLine(); sb.AppendLine(contentBody.ToString()); } break; default: sb.AppendFormat("<html><head><title>You request is {0}</title></head><body><h2>Welcome to my http server</h2>{1}<h3>Author: <a href='mailto:http://www.baidu.com/'>百度</a></h3></body></html>", request.Url, request.ToString()); break; } } return localServerEncoding.GetBytes(sb.ToString()); } /// <summary> /// 根据Url调用对应的接口方法处理数据 /// </summary> /// <param name="request"></param> /// <returns>JSON格式字符串</returns> private string DealWithUrl(MyHTTPRequest request) { string returnValue = "未找到对应的接口方法:" + request.Url; switch (request.Url)//.ToUpper()) { case "/Local.GetInfo": returnValue = GetInfo(); case "/Local.WorkWithInput": returnValue = WorkWithInput(request.Parameter); break; break; default: break; } return returnValue; } #region Local.xxx 表示提供给外部系统调用的接口 /// <summary> /// 1)获取**信息 /// </summary> /// <returns></returns> public string GetInfo() { } #endregion public void WriteFailure(StringBuilder sw) { sw.AppendLine("http/1.0 404 file not found"); sw.AppendLine("connection: close"); sw.AppendLine(); } #region Target.xxxx 表示本地系统调用外部系统的接口 private string UploadInfo(UploadInput input) { } #endregion private string Post(string postUrl, string paramData) { string result = string.Empty; Stream stream = null; StreamReader sr = null; HttpWebResponse response = null; try { byte[] byteArray = Encoding.UTF8.GetBytes(paramData); HttpWebRequest webReq = (HttpWebRequest)WebRequest.Create(new Uri(postUrl)); webReq.Method = "POST"; webReq.ContentType = localServerContentType; //webReq.ContentType = "application/x-www-form-urlencoded"; webReq.Accept = "text/json"; webReq.UserAgent = localServerInfo; webReq.ContentLength = byteArray.Length; webReq.ServicePoint.Expect100Continue = false; webReq.CookieContainer = CookieContainer; stream = webReq.GetRequestStream(); stream.Write(byteArray, 0, byteArray.Length); response = (HttpWebResponse)webReq.GetResponse(); if (response.StatusCode == HttpStatusCode.OK) { response.Cookies = CookieContainer.GetCookies(webReq.RequestUri); sr = new StreamReader(response.GetResponseStream(), Encoding.UTF8); result = sr.ReadToEnd(); } else { Debug.WriteLine(response.StatusCode); } } finally { if (sr != null) { sr.Close(); } if (response != null) { response.Close(); } if (stream != null) { stream.Close(); } } return result; } public static bool IsNullOrWhiteSpace(string value) { if (value != null) { for (int i = 0; i < value.Length; i++) { if (!char.IsWhiteSpace(value[i])) { return false; } } } return true; } #region JSONObject转换 public static string ToJSON(object value) { return Newtonsoft.Json.JsonConvert.SerializeObject(value, Formatting.Indented, new JsonSerializerSettings { ContractResolver = new LowerCasePropertyNamesContractResolver(), DateFormatString = "yyyy-MM-dd HH:mm:ss.fff", ReferenceLoopHandling = ReferenceLoopHandling.Serialize }); } private class LowerCasePropertyNamesContractResolver : DefaultContractResolver { public LowerCasePropertyNamesContractResolver() : base(true) { } protected override string ResolvePropertyName(string propertyName) { return propertyName.ToLower(); } } public static T ToObject<T>(string json) { return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(json); //return fastJSON.JSON.ToObject<T>(json); } #endregion }
基于Socket的服务器程序基本完成。