iOS 使用 socket 即时通信(非第三方库)
tianmaxk
8年前
<p>其实写这个socket一开始我是拒绝的。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/0f7ad2c5290593677e8b862a509f10bb.jpg"></p> <p>因为大家学C 语言和linux基础时肯定都有接触,客户端和服务端的通信也都了解过,加上现在很多开放的第三方库都不需要我们来操作底层的通信。</p> <p>但是来了!!!</p> <p>但是!还是想写。底层的东西最好了解下。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/1d6bdc33a535c0eb7563cf9e69034237.jpg"></p> <p>好了 正经了!!!!</p> <h2>效果</h2> <p style="text-align:center"><img src="https://simg.open-open.com/show/72971a2e04cc1472cdbad428626a7088.gif"></p> <p style="text-align:center">效果.gif</p> <p>由于5M的上传限制GIF可能看不清 我再截两张图吧</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/508f263682b3ace9332c27406620c2aa.png"></p> <p style="text-align:center">服务器</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/3419d91cf312a4630d8835b0866fc920.png"></p> <p style="text-align:center">客户端A</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/a20be1166725dccb2761fca829100c05.png"></p> <p style="text-align:center">客户端B</p> <h2>模型图</h2> <p style="text-align:center"><img src="https://simg.open-open.com/show/c2780407e05a3bd15bcc8444b3caafea.jpg"></p> <p>做了个逗比模型图:arrow_down:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/f79efd4df584829381307c732a31d4d5.png"></p> <p style="text-align:center">模型</p> <h2>分析</h2> <p>由上图可以了解到服务器和客户端需要做哪些工作</p> <h2>服务器</h2> <p>抽象一点分为:</p> <ul> <li>1.创建线程等待接收客户端的连接</li> <li>2.接收并解析客户端发来的消息</li> <li>3.给客户端发送消息<br> 具体一点:</li> <li>1.创建socket. 绑定端口.开始监听.</li> <li>2.创建线程.等待接收客户端连接.</li> <li>3.接收客户端发来的消息</li> <li>4.解析消息内容 <ul> <li>a.设置用户名</li> <li>b.发送消息给指定客户端</li> </ul> </li> </ul> <h2>客户端</h2> <p>抽象一点分为:</p> <ul> <li>1.连接服务器</li> <li>2.给服务器发送消息</li> <li>3.接收服务器消息</li> <li>4.解析消息内容<br> 具体一点:</li> <li>1.创建socket.绑定端口.连接服务器</li> <li>2.发送消息 <ul> <li>a.设置用户名</li> <li>b.给指定用户发消息:按服务器格式拼接字符串</li> </ul> </li> <li>3.接收消息 <ul> <li>a.普通消息</li> <li>b.用户列表:保存至用户列表</li> </ul> </li> </ul> <h2>UI方面</h2> <p>服务器:其实不用什么UI放个控件展示下日志就是了</p> <p>客户端:比较简单,一个俗套聊天室的界面,直接storyboard里拖拖控件设置约束了</p> <p>DEMO而已别太当真</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/17fc90c0bf2b06360e18db643d2c49e9.jpg"></p> <h2>代码部分</h2> <h2>服务器</h2> <p>要使用scoket需要引用这三个头文件</p> <pre> <code class="language-objectivec">#include <netinet/in.h> #include <sys/socket.h> #include <arpa/inet.h></code></pre> <p>只有一个model,用来绑定用户名和socket</p> <pre> <code class="language-objectivec">@interface ClientModel : NSObject @property(nonatomic,assign)int clientSocket; @property(nonatomic,copy)NSString *clientName; @end</code></pre> <p>只有一个文件全给你</p> <pre> <code class="language-objectivec">#import "ViewController.h" #include <netinet/in.h> #include <sys/socket.h> #include <arpa/inet.h> #import "ClientModel.h" static int const kMaxConnectCount = 5; @interface ViewController() @property (weak) IBOutlet NSTextField *textField; //@property (nonatomic,assign)int client_socket; //客户端socket @property (unsafe_unretained) IBOutlet NSTextView *textView; @property (nonatomic,strong)NSMutableArray *clientArray; @property (nonatomic,strong)NSMutableArray *clientNameArray; @end @implementation ViewController - (NSMutableArray *)clientArray { if (!_clientArray) { _clientArray = [NSMutableArray array]; } return _clientArray; } - (NSMutableArray *)clientNameArray { if (!_clientNameArray) { _clientNameArray = [NSMutableArray array]; } return _clientNameArray; } - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. //创建socket int server_socket = socket(AF_INET, SOCK_STREAM, 0); if (server_socket == -1) { NSLog(@"创建失败"); [self showLogsWithString:@"socket创建失败"]; }else{ //绑定地址和端口 struct sockaddr_in server_addr; server_addr.sin_len = sizeof(struct sockaddr_in); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(1234); server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); bzero(&(server_addr.sin_zero), 8); int bind_result = bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)); if (bind_result == -1) { NSLog(@"绑定端口失败"); [self showLogsWithString:@"绑定端口失败"]; }else{ if (listen(server_socket, kMaxConnectCount)==-1) { NSLog(@"监听失败"); [self showLogsWithString:@"监听失败"]; }else{ for (int i = 0; i < kMaxConnectCount; i++) { //接受客户端的链接 [self acceptClientWithServerSocket:server_socket]; } } } } } - (void)setRepresentedObject:(id)representedObject { [super setRepresentedObject:representedObject]; // Update the view, if already loaded. } //创建线程接受客户端 -(void)acceptClientWithServerSocket:(int)server_socket{ struct sockaddr_in client_address; socklen_t address_len; dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(queue, ^{ //创建新的socket while (1) { int client_socket = accept(server_socket, (struct sockaddr*)&client_address,&address_len ); if (client_socket == -1) { [self showLogsWithString:@"接受客户端链接失败"]; NSLog(@"接受客户端链接失败"); }else{ NSString *acceptInfo = [NSString stringWithFormat:@"客户端 in,socket:%d",client_socket]; [self showLogsWithString:acceptInfo]; //接受客户端数据 [self recvFromClinetWithSocket:client_socket]; } } }); } //接受客户端数据 - (void)recvFromClinetWithSocket:(int)client_socket{ while (1) { //接受客户端传来的数据 char buf[1024] = {0}; long iReturn = recv(client_socket, buf, 1024, 0); if (iReturn>0) { NSLog(@"客户端来消息了"); NSString *str = [NSString stringWithCString:buf encoding:NSUTF8StringEncoding]; [self showLogsWithString:[NSString stringWithFormat:@"客户端来消息了:%@",str]]; [self checkRecvStr:str andClientSocket:client_socket]; }else if (iReturn == -1){ NSLog(@"读取消息失败"); [self showLogsWithString:@"读取消息失败"]; break; }else if (iReturn == 0){ NSLog(@"客户端走了"); [self showLogsWithString:[NSString stringWithFormat:@"客户端 out socket:%d",client_socket]]; NSMutableArray *array = [NSMutableArray arrayWithArray:self.clientArray]; for (ClientModel *model in array) { if (model.clientSocket == client_socket) { [self.clientNameArray removeObject:model.clientName]; [self.clientArray removeObject:model]; } } close(client_socket); break; } } } //检查接受到的字符串 - (void)checkRecvStr:(NSString*)str andClientSocket:(int)socket{ if ([str hasPrefix:@"name:"]) { NSString *name = [str substringFromIndex:5]; ClientModel *model = [[ClientModel alloc] init]; model.clientSocket = socket; model.clientName = name; if (self.clientArray.count > 0) { int flag = 999; //用户名不能相同 int i = 0; for (ClientModel *client in self.clientArray) { //改名 if (client.clientSocket == socket) { NSString *oldName = self.clientNameArray[i]; self.clientNameArray[i] = name; self.clientArray[i] = model; for (ClientModel *oldclient in self.clientArray) { [self sendMsg:[NSString stringWithFormat:@"%@ 改名 %@",oldName,name] toClient:oldclient.clientSocket]; [self showLogsWithString:[NSString stringWithFormat:@"%@ 改名 %@",oldName,name]]; NSString *list = [self.clientNameArray componentsJoinedByString:@","]; //向客户端推送当前在线列表 [self sendMsg:[NSString stringWithFormat:@"list:%@",list] toClient:oldclient.clientSocket]; } flag = 2; }else{ if ([client.clientName isEqualToString:model.clientName]) { //用户名已存在 flag = 1; break; } } i++; } if (flag != 1 & flag != 2) { [self.clientArray addObject:model]; [self.clientNameArray addObject:model.clientName]; //向客户端推送当前在线列表 for (ClientModel *client in self.clientArray) { [self sendMsg:[NSString stringWithFormat:@"%@,上线了",name] toClient:client.clientSocket]; NSString *list = [self.clientNameArray componentsJoinedByString:@","]; //向客户端推送当前在线列表 [self sendMsg:[NSString stringWithFormat:@"list:%@",list] toClient:client.clientSocket]; } //给当前客户端发送一条欢迎信息 NSString *msg = [NSString stringWithFormat:@"Welcome %@ !",name]; [self sendMsg:msg toClient:socket]; [self showLogsWithString:msg]; }else if (flag == 1){ [self sendMsg:@"注册用户名失败,用户名已经存在,请重新设置用户名" toClient:socket]; [self showLogsWithString:[NSString stringWithFormat:@"socket %d 注册用户名失败,设置的用户名已经存在",socket]]; for (ClientModel *model in self.clientArray) { [name isEqualToString:model.clientName]; } } }else{ [self.clientArray addObject:model]; [self.clientNameArray addObject:model.clientName]; //向客户端推送当前在线列表 //给当前客户端发送一条欢迎信息 NSString *msg = [NSString stringWithFormat:@"Welcome %@ !",name]; [self sendMsg:msg toClient:socket]; [self showLogsWithString:msg]; NSString *list = [self.clientNameArray componentsJoinedByString:@","]; //向客户端推送当前在线列表 [self sendMsg:[NSString stringWithFormat:@"list:%@",list] toClient:socket]; } } //给某人发消息 else if ([str hasPrefix:@"to:"]){ NSRange nameRange = [str rangeOfString:@"*"]; NSString *name = [str substringWithRange:NSMakeRange(3, nameRange.location-3)]; NSString *content = [str substringFromIndex:nameRange.location+1]; NSString *fromClientName; //找出发送者 for (ClientModel *model in self.clientArray) { if (socket == model.clientSocket) { fromClientName = model.clientName; break; } } //给目标发送信息 for (ClientModel *model in self.clientArray) { if ([name isEqualToString:model.clientName]) { NSString *msg = [NSString stringWithFormat:@"%@ to you\n%@",fromClientName,content]; [self sendMsg:msg toClient:model.clientSocket]; [self showLogsWithString:[NSString stringWithFormat:@"%@ 发送给 %@ 内容是:%@",fromClientName,name,content]]; break; } } } } //给客户端发送信息 - (void)sendMsg:(NSString*)msg toClient:(int)socket{ char *buf[1024] = {0}; const char *p1 = (char*)buf; p1 = [msg cStringUsingEncoding:NSUTF8StringEncoding]; send(socket, p1, 1024, 0); } //在界面上显示日志 - (void)showLogsWithString:(NSString*)str { dispatch_async(dispatch_get_main_queue(), ^{ NSString *newStr = [NSString stringWithFormat:@"\n%@",str]; self.textView.string = [self.textView.string stringByAppendingString:newStr]; }); } @end</code></pre> <h2>客户端</h2> <p>由于客户端设计的就比较简单,所以代码量也很少,全给你.</p> <pre> <code class="language-objectivec">#import "ViewController.h" #include <netinet/in.h> #include <sys/socket.h> #include <arpa/inet.h> @interface ViewController ()<UITableViewDelegate,UITableViewDataSource> //服务器socket @property (nonatomic,assign)int server_socket; //UI @property (weak, nonatomic) IBOutlet UITextField *userNameField; @property (weak, nonatomic) IBOutlet UITextView *chatView; @property (weak, nonatomic) IBOutlet UITextField *msgField; @property (weak, nonatomic) IBOutlet UILabel *toName; @property (weak, nonatomic) IBOutlet UIView *onlineUserView; @property (nonatomic,strong)UITableView *onlineTable; //user列表 @property (nonatomic,strong)NSMutableArray *userArray; @end @implementation ViewController - (NSMutableArray *)userArray { if (!_userArray) { _userArray = [NSMutableArray array]; } return _userArray; } - (void)viewDidLoad { [super viewDidLoad]; [self.userNameField becomeFirstResponder]; self.userNameField.text = @""; self.msgField.text = @""; //添加table用户列表 self.onlineTable = [[UITableView alloc] initWithFrame:self.onlineUserView.frame style:UITableViewStylePlain]; self.onlineTable.delegate = self; self.onlineTable.dataSource = self; [self.view addSubview:self.onlineTable]; int server_socket = socket(AF_INET, SOCK_STREAM, 0); if (server_socket == -1) { NSLog(@"创建失败"); }else{ //绑定地址和端口 struct sockaddr_in server_addr; server_addr.sin_len = sizeof(struct sockaddr_in); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(1234); server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); bzero(&(server_addr.sin_zero), 8); //接受客户端的链接 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(queue, ^{ //创建新的socket int aResult = connect(server_socket, (struct sockaddr*)&server_addr, sizeof(struct sockaddr_in)); if (aResult == -1) { NSLog(@"链接失败"); }else{ self.server_socket = server_socket; [self acceptFromServer]; } }); } } //从服务端接受消息 - (void)acceptFromServer{ while (1) { //接受服务器传来的数据 char buf[1024]; long iReturn = recv(self.server_socket, buf, 1024, 0); if (iReturn>0) { NSString *str = [NSString stringWithCString:buf encoding:NSUTF8StringEncoding]; //筛选前缀 if ([str hasPrefix:@"list:"]) { NSString *arrayStr = [str substringFromIndex:5]; NSArray *list = [arrayStr componentsSeparatedByString:@","]; self.userArray = [NSMutableArray arrayWithArray:list]; dispatch_async(dispatch_get_main_queue(), ^{ [self.onlineTable reloadData]; }); NSLog(@"当前在线用户列表:%@",arrayStr); }else{ //回到主线程 界面上显示内容 [self showLogsWithString:str]; } }else if (iReturn == -1){ NSLog(@"接受失败-1"); break; } } } //在界面上显示日志 - (void)showLogsWithString:(NSString*)str { dispatch_async(dispatch_get_main_queue(), ^{ NSString *newStr = [NSString stringWithFormat:@"\n%@",str]; self.chatView.text = [self.chatView.text stringByAppendingString:newStr]; }); } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. } //设置用户名 - (IBAction)clickSetUserName:(id)sender { NSString *msg = [NSString stringWithFormat:@"name:%@",self.userNameField.text] ; [self sendMsg:msg]; // [self showLogsWithString:msg]; [self.msgField becomeFirstResponder]; } //发送信息 - (IBAction)clickSendMsg:(id)sender { if ([self.msgField.text isEqualToString:@""] || ![self.userArray containsObject:self.userNameField.text] || [self.toName.text isEqualToString:self.userNameField.text]) { [self showLogsWithString:@"请设置用户名、检查发送对象、消息不能为空"]; return; } NSString *msg = [NSString stringWithFormat:@"to:%@*%@",self.toName.text,self.msgField.text]; [self sendMsg:msg]; NSString *displayMsg = [NSString stringWithFormat:@"to:%@\n%@",self.toName.text,self.msgField.text]; [self showLogsWithString:displayMsg]; self.msgField.text = @""; } //给客户端发送信息 - (void)sendMsg:(NSString*)msg { char *buf[1024] = {0}; const char *p1 = (char*)buf; p1 = [msg cStringUsingEncoding:NSUTF8StringEncoding]; send(self.server_socket, p1, 1024, 0); } #pragma mark - TableViewDelegate & dataSource - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ return self.userArray.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ static NSString *cellId = @"onlinetableviewcellid"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellId]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellId]; }else{ NSLog(@"cell重用了"); } cell.textLabel.text = self.userArray[indexPath.row]; return cell; } //点击cell - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ self.toName.text = self.userArray[indexPath.row]; [self.msgField becomeFirstResponder]; } @end</code></pre> <p> </p>