环境
- Xcode 11.6
- iOS 13
- MacOS 10.15
导航
完整代码在此,熟悉的小伙伴可以直接试试。
UI界面
这篇,我们将构建一个运行在mac上的Server作为代理服务器,接收Client发送过来的UDP数据,再转发。
也会有一个简单的界面,如下:
image添加一个新的Target:Server,macOS App使用SwiftUI,
然后还必须勾选Network的2个权限:
imageContentView代码:
import SwiftUI
struct ContentView: View {
@State var items: [YYListView.Model] = []
var body: some View {
VStack {
HStack {
Text("接收数据中...")
Button(action: clean) {
Text("clean")
}
}.fixedSize()
YYListView(items: $items)
}.onAppear(perform: starServer)
}
private func clean() {
items.removeAll()
}
private func starServer() {
YYServer.startUDPServer(8899) { str in
self.items.append(.init(text: str))
}
}
}
YYListView:
import SwiftUI
extension YYListView {
struct Model: Identifiable {
var id = UUID()
var text: String
}
}
struct YYListView: View {
@Binding var items: [Model]
var body: some View {
List(items) { item in
Text(item.text)
}
}
}
可以看到,核心方法就是starServer了。
大概意思就是开启一个端口8899的服务器,并设置回调,将收到的字符串添加到items数组,刷新UI。
private func starServer() {
YYServer.startUDPServer(8899) { str in
self.items.append(.init(text: str))
}
}
Socket编程
先来把服务器写好,这里打算使用C的Socket来创建服务器,简单强大,而且mac上也可以用,直接上代码吧:
udp_server.h
#ifndef udp_server_h
#define udp_server_h
#include <stdio.h>
typedef void (*data_handler_t)(char *, long);
#define ANET_OK 0
#define ANET_ERR -1
void udp_server_start(int port);
void set_data_handler(data_handler_t handler);
#endif /* udp_server_h */
对外提供开启服务器和设置数据监听的api。
udp_server.c
#include "udp_server.h"
#include <stdio.h>
#include <ctype.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netdb.h>
#include <errno.h>
#include <stdlib.h>
#include <time.h>
#include <arpa/inet.h>
#include <pthread/pthread.h>
#include <net/if.h>
#define BUFF_LEN 1500
data_handler_t datahandler;
void set_data_handler(data_handler_t handler) {
datahandler = handler;
}
void handle_udp_datagram(int fd) {
char buf[BUFF_LEN];
long count;
struct sockaddr_in client;
socklen_t len;
while (1) {
bzero(buf, sizeof(buf));
len = sizeof(client);
count = recvfrom(fd, buf, BUFF_LEN, 0, (struct sockaddr *)&client, &len);
if (count == ANET_ERR) {
printf("recieve data failed:[%s]\n", strerror(errno));
return;
}
if (datahandler != NULL) {
datahandler(buf, count);
}
sendto(fd, buf, count, 0, (struct sockaddr*)&client, len);
}
}
static int upd_server(int port) {
///AF_INET:IPV4;SOCK_DGRAM:UDP
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) {
printf("creating socket failed:[%s]\n", strerror(errno));
return ANET_ERR;
}
struct sockaddr_in sa;
bzero(&sa, sizeof(sa));
sa.sin_family = AF_INET;
///IP地址,需要进行网络序转换,INADDR_ANY:本地地址
sa.sin_addr.s_addr = htonl(INADDR_ANY);
sa.sin_port = htons(port);
int ret = bind(fd, (struct sockaddr *)&sa, sizeof(sa));
if (ret < 0) {
printf("bind failed:[%s]\n", strerror(errno));
close(fd);
return ANET_ERR;
}
return fd;
}
void udp_server_start(int port) {
int fd = upd_server(port);
if (fd > 0) {
handle_udp_datagram(fd);
close(fd);
}
}
简单起见,这里直接使用recvfrom循环阻塞接受数据,而且先把收到的数据直接发送信息给client,走通流程。
Objc桥接
服务器写好了,现在我们要在Swift里面使用,虽然有些麻烦,不过Swift是可以调用C的;
但是这种时候,我还是喜欢先用Objc封装一下,毕竟这是Objc的优势之一,而且封装好后,直接放入Objc项目也很方便。
YYServer.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef void (^DataHandler)(NSString *data);
@interface YYServer : NSObject
+ (void)startUDPServer:(NSInteger)port dataHandler:(DataHandler)handler;
@end
NS_ASSUME_NONNULL_END
YYServer.m
#import "YYServer.h"
#include "udp_server.h"
int port = 8899;
DataHandler dataHandler;
@implementation YYServer
void data_handler(char *udpbuf, long len) {
if (dataHandler) {
NSData *udpData = [NSData dataWithBytes:udpbuf length:len];
NSString *data = udpData.description;
NSLog(@"data_handler ------- %@", data);
dispatch_async(dispatch_get_main_queue(), ^{
dataHandler(data);
});
}
}
+ (void)startUDPServer:(NSInteger)port dataHandler:(DataHandler)handler {
dataHandler = handler;
set_data_handler(data_handler);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
udp_server_start((int)port);
});
}
@end
这里需要注意,因为我们的server会循环阻塞的接收数据,所以不能在主线程启动,当然,这个是应该优化的- -
测试
现在应该能跑通了,来测试一下。
先运行Server,端口8899,然后看一下电脑连上wifi的ip地址,比如我这里172.20.49.36,
然后在Client的ConfigView中,将下面的hostname和port设置成自己的,
@ObservedObject var viewModel =
ConfigViewModel(config: .init(hostname: "172.20.49.36", port: "8899"))
还有YYVPNManager中的这2个ID也必须设置成自己的,
extension YYVPNManager {
public static let groupID = "group.com.yy.Client"
public static let bundleIDTunnel = "com.yy.Client.Tunnel"
}
运行Client,将手机和电脑连上同一个wifi,点击Start,顺利的话,应该就能看到第一张图的内容了😄~~
不顺利的话,一般都是Tunnel进程或Server权限问题,用Console调试一下吧😂。。
ok之后你会发现,手机里的所有应用都无法获取数据了,一直在loading,直到超时😂😂😂,
这是因为我们现在的服务器是直接把客户端请求的包原路返回了,漏掉了去请求真正服务器这步,
这个后面再优化,现在调通后,还剩下怎么把Tunnel进程中的数据发送给主App了,我们下篇继续~
网友评论