0%

若教眼底无离恨,不信人间有白头。

composer insatall が失敗す

PHP のライブラリの依存関係を管理する composer ですが、composer install でインストールをしようとすると killed で失敗してしまうのでその原因を調べました。
ちなみに Laravel の環境を作っている最中でした。

现象

1
2
3
4
$ composer install
Loading composer repositories with package information
Updating dependencies (including require-dev)
Killed

パッケージをインストールしようとすると上記のような感じで killed で中途半端に終わります。composer install -vvv で詳細な進捗が表示されるので実行して見てみると、順調に進んでいる途中でプチっと途切れてしまいます.

原因

原因はメモリ不足です。

どうも killed で終わるのは Linux がメモリ不足でシステム停止する恐れがあるときに、メモリを多く消費しているプロセスを強制的に殺しているかららしいです。OOM Killer という機能です。

composer install を実行すると、依存関係をいろいろと調整したりするためにデータを読み込んだりするのですが、それが大きくなるとメモリの消費量が大きくなりすぎます。そして限界がきて OS からプロセスが殺されます。

解决

原因がメモリ不足なのでメモリの割り当てを増やすことで解決できます。

メモリを増やすのが難しければ、スワップ領域を増やしてやればよいです。私はスワップ領域に1GB分割り当てて解決しました。

1
2
3
# dd if=/dev/zero of=/var/swap bs=1M count=1024
# mkswap /var/swap
# swapon /var/swap

/var/swap ファイルに 1024MB(1GB)分のファイルを作って割り当てています。これで再実行すれば正常に完了するはずです。

以上。

https://webbibouroku.com/Blog/Article/composer-killed

寂寞空庭春欲晚,梨花满地不开门。
大多数我们是不知道在何时删除了某个文件,通过下面这个命令我们可以查看在哪个 commit 中删除了哪些文件。
1
git log --diff-filter=D --summary
执行这个命令后效果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
commit 6dcf1e6332e2f0cc10902b995a6efda72b88ebc4
Author: 某某 <lnmput@gmail.com>
Date: Fri Nov 16 17:58:27 2018 +0800

修改页面

delete mode 100644 public/themes/lal/fdssf.php

commit cef7eed4b38360a4f3f6ea70c173654df30c486f
Author: 某某 <lnmput@gmail.com>
Date: Wed Nov 7 17:25:44 2018 +0800

调整代码结构

delete mode 100644 app/Http/Controllers/Homes.php
delete mode 100644 app/Http/Requests/AddressAddRequest.php
delete mode 100644 app/Http/Requests/Web/AddressStoreRequest.php

commit 572110f5deed9d3ff76ccc9a7da75c0e5ed324ce
Author: 某某 <lnmput@gmail.com>
Date: Wed Nov 7 00:04:51 2018 +0800

更新缓存

delete mode 100644 app/Console/Commands/InitCommand.php
比如我想恢复 ic_selected.png 这个文件,我们可以看到删除该文件对应的 commit id :f541888b0e7255cc6aa22a277f3dd9fe5502e5e2。

接下来我们执行下面这个命令

1
git checkout $commit~1 filename

这个命令会检出该 commit 的上一个提交中的文件,因为我们是在该 commit 中删除的文件,所以需要在上一个 commit 才能恢复出文件。

https://www.jianshu.com/p/41ad0dcfd8da

若教眼底无离恨,不信人间有白头。

Step 1: Install Socialite Package

In first step we will install Socialite Package that provide fb api to connect with facebook. So, first open your terminal and run bellow command:

1
composer require laravel/socialite

After install above package we should add providers and aliases in config file, Now open config/app.php file and add service provider and alias.

1
2
3
4
5
6
7
8
'providers' => [
....
Laravel\Socialite\SocialiteServiceProvider::class,
],
'aliases' => [
....
'Socialite' => Laravel\Socialite\Facades\Socialite::class,
],

Step 2: Create Facebook App

In this step we need facebook app id and secret that way we can get information of other user. so if you don’t have facebook app account then you can create from here : https://developers.facebook.com/apps and after create account you can copy client id and secret.
Now you have to set app id, secret and call back url in config file so open config/services.php and .env file then set id and secret this way:
config/services.php

1
2
3
4
5
6
7
8
return [
....
'facebook' => [
'client_id' => env('FACEBOOK_CLIENT_ID'),
'client_secret' => env('FACEBOOK_CLIENT_SECRET'),
'redirect' => env('FACEBOOK_CALLBACK_URL'),
],
]

.env

1
2
3
FACEBOOK_CLIENT_ID=xxxxxxxxx
FACEBOOK_CLIENT_SECRET=xxxxxxx
FACEBOOK_CALLBACK_URL=http://localhost:8000/auth/facebook/callback

Step 3: Create Migration and Model

In this step first we have to create migration for add facebook_id in your user table. so let’s create new migration and bellow column this way:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddNewColunmUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table("users", function (Blueprint $table) {
$table->string('facebook_id');
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table("users", function (Blueprint $table) {
$table->dropColumn('facebook_id');
});
}
}

Now add addNew() in User model, that method will check if facebook id already exists then it will return object and if not exists then create new user and return user object. so open user model and put bellow code:
app/User.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php
namespace App;

use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
use Notifiable;


/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'password', 'facebook_id'
];


/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];


public function addNew($input)
{
$check = static::where('facebook_id',$input['facebook_id'])->first();


if(is_null($check)){
return static::create($input);
}


return $check;
}
}

Step 4: Create New Routes

In this step we need to create routes for facebook login, so you need to add following route on bellow file.
routes/web.php

1
2
3
4
5
Route::get('facebook', function () {
return view('facebook');
});
Route::get('auth/facebook', 'Auth\FacebookController@redirectToFacebook');
Route::get('auth/facebook/callback', 'Auth\FacebookController@handleFacebookCallback');

Step 5: Create New FacebookController

we need to add new controller and method of facebook auth that method will handle facebook callback url and etc, first put bellow code on your FacebookController.php file.
app/Http/Controllers/Auth/FacebookController.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php
namespace App\Http\Controllers\Auth;

use App\User;
use App\Http\Controllers\Controller;
use Socialite;
use Exception;
use Auth;

class FacebookController extends Controller
{
/**
* Create a new controller instance.
*
* @return void
*/
public function redirectToFacebook()
{
return Socialite::driver('facebook')->redirect();
}

/**
* Create a new controller instance.
*
* @return void
*/
public function handleFacebookCallback()
{
try {
$user = Socialite::driver('facebook')->user();
$create['name'] = $user->getName();
$create['email'] = $user->getEmail();
$create['facebook_id'] = $user->getId();

$userModel = new User;
$createdUser = $userModel->addNew($create);
Auth::loginUsingId($createdUser->id);
return redirect()->route('home');
} catch (Exception $e) {
return redirect('auth/facebook');
}
}
}

Step 6: Create Blade File

Ok, now at last we need to add blade view so first create new file facebook.blade.php file and put bellow code:
resources/views/facebook.blade.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@extends('layouts.app')


@section('content')
<div class="container">
<div class="row">
<div class="col-md-12 row-block">
<a href="{{ url('auth/facebook') }}" class="btn btn-lg btn-primary btn-block">
<strong>Login With Facebook</strong>
</a>
</div>
</div>
</div>
@endsection

Ok, now you are ready to use open your browser and check here : URL + ‘/facebook’.

I hope it can help you…..

https://itsolutionstuff.com/post/laravel-56-login-with-facebook-with-socialiteexample.html

受任于败军之际,奉命于危难之间,尔来二十有一年矣。

當一個新模型初次被儲存,將會觸發 creating 以及 created 事件。如果一個模型已經存在於資料庫而且呼叫了 save 方法,將會觸發 updating 和 updated 事件。然而,在這兩個狀況下,都將會觸發 saving 和 saved 事件。

Laravel 在資料更新的時候會觸發兩個事件:updated 和 saved。單從官方文件的說明,saved 看起來像是一個方便開發者統一管理新增/更新動作的事件,讓重複的程式碼可以不用被寫在兩個地方(created 和 updated),但是其實 saved 和 updated 的觸發時機是不一樣的。

接來看一下 Laravel eloquent 的原始碼。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 495 行
public function save(array $options = [])
{
...
if ($this->exists) {
$saved = $this->isDirty() ?
$this->performUpdate($query) : true;
}
...
if ($saved) {
$this->finishSave($options);
}
return $saved;
}
// 552 行
protected function finishSave(array $options)
{
$this->fireModelEvent('saved', false);
$this->syncOriginal();
if (Arr::get($options, 'touch', true)) {
$this->touchOwners();
}
}
// 569 行
protected function performUpdate(Builder $query)
{
...
$dirty = $this->getDirty();
if (count($dirty) > 0) {
$this->setKeysForSaveQuery($query)->update($dirty);
$this->fireModelEvent('updated', false);
}
return true;
}

應該盡量使用 updated 而不是 saved 來判斷資料是否更新
可以看到在觸發 updated 之前會先做一次 getDirty() 的檢查,其實就是在檢查這次更新的資料是不是真的有改動資料的值;相反的,saved 不管值有沒有改動都會觸發。如果沒注意到這個差別的話,這在實務上其實會造成一些問題。

例如,我們通常會把資料的結果快取起來,以減少對資料庫的查詢次數。因此當資料有變動的時候,就會需要把對應的快取刪除。為了避免有漏刪的部分,會把刪除快取的動作綁定在 eloquent 事件中。認清 updated 和 saved 之後,就會知道這個動作應該是要綁在 updated;若是綁在 saved 上,就會造成多餘的快取動作,增加機器的成本。當快取數量一多,又有用到類似 redis scan 這種較耗時的功能來找出需要刪除的快取時,對於機器的效能影響更大。

使用 updated 也仍會可能有漏網之魚

但是,要注意的是,即使使用了 updated 也不能說完全避免這件事的發生。這時候又要再看一次 Larvel eloquent 的原始碼了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 3093 行
public function getDirty()
{
$dirty = [];
foreach ($this->attributes as $key => $value) {
if (! array_key_exists($key, $this->original)) {
$dirty[$key] = $value;
} elseif ($value !== $this->original[$key] &&
! $this->originalIsNumericallyEquivalent($key)) {
$dirty[$key] = $value;
}
}
return $dirty;
}

可以發現 Laravel 判斷值有沒有改動的比較依據來自於,我們一開始 select 出來的欄位,跟這次要變動的欄位。意思就是,如果我們只 select 了 A 欄位出來,但是更新的是 B 欄位時,即使更動後 B 欄位的值跟更動之前一模一樣,Laravel 仍會把此視為資料的值有所改動。

不過這仍在可以接受的範圍之內。畢竟每個更新之前,若要為了拿到所有欄位而又再多查詢一次資料庫,可能會是一件更浪費資源的事情。

https://medium.com/@lynnlin827/laravel-eloquent-%E4%BA%8B%E4%BB%B6%E4%B8%AD%E7%9A%84-updated-%E5%92%8C-saved-%E5%8E%9F%E4%BE%86%E4%B8%8D%E4%B8%80%E6%A8%A3-caa8ef0ddbc

You know some birds are not meant to be caged, their feathers are just too bright.

File uploads is one of the most important functions on the internet, and we have bigger files nowadays, which means it’s not enough to have simple input fields – we need AJAX and processing file upload “in the background”. Here I will show you a simple example of that in Laravel 5.

Let’s say, we have a simple form to upload the product and many photos for it.

And we want to upload photos and see upload progress immediately, only then submitting the form. For that we will use a jQuery-File-Upload library.

Step 1. Database structure

1
2
3
4
5
6
7
8
9
10
11
12
13
Schema::create('products', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->timestamps();
});

Schema::create('product_photos', function (Blueprint $table) {
$table->increments('id');
$table->integer('product_id')->unsigned()->nullable();
$table->foreign('product_id')->references('id')->on('products');
$table->string('filename');
$table->timestamps();
});

And then we have two simple models – app/Product.php and app/ProductPhoto.php.

1
2
3
4
class Product extends Model
{
protected $fillable = ['name'];
}
1
2
3
4
5
6
7
8
9
class ProductPhoto extends Model
{
protected $fillable = ['product_id', 'filename'];

public function product()
{
return $this->belongsTo('App\Product');
}
}

As you can see, field product_photos.product_id is nullable – which means we can upload photos without saving product with them yet. We’ll show later why.

Step 2. Routes and MVC

First, let’s decide our URLs. In routes/web.php we will have this:

1
2
3
4

Route::get('/', 'UploadController@uploadForm');
Route::post('/upload', 'UploadController@uploadSubmit');
Route::post('/product', 'UploadController@postProduct');

Basically, homepage for the form, then /upload for AJAX file submit, and /product for submitting the whole product with photos.

Then we have app/Http/Controllers/UploadController.php with these methods:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

public function uploadForm()
{
return view('upload_form');
}

public function uploadSubmit(Request $request)
{
// This method will cover file upload
}

public function postProduct(Request $request)
{
// This method will cover whole product submit
}

Step 3. Building the form

Our resources/views/upload_form.blade.php will look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

<form action="/product" method="post">
{{ csrf_field() }}
Product name:
<br />
<input type="text" name="name" />
<br /><br />
Product photos (can add more than one):
<br />
<input type="file" id="fileupload" name="photos[]" data-url="/upload" multiple />
<br />
<div id="files_list"></div>
<p id="loading"></p>
<input type="hidden" name="file_ids" id="file_ids" value="" />
<input type="submit" value="Upload" />
</form>

Step 4. Processing the upload and submit

Now, let’s download jQuery-File-Upload library and put its /js contents into our /public/js. And then we can use it like this – in the end of our upload_form.blade.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="/js/vendor/jquery.ui.widget.js"></script>
<script src="/js/jquery.iframe-transport.js"></script>
<script src="/js/jquery.fileupload.js"></script>
<script>
$(function () {
$('#fileupload').fileupload({
dataType: 'json',
add: function (e, data) {
$('#loading').text('Uploading...');
data.submit();
},
done: function (e, data) {
$.each(data.result.files, function (index, file) {
$('<p/>').html(file.name + ' (' + file.size + ' KB)').appendTo($('#files_list'));
if ($('#file_ids').val() != '') {
$('#file_ids').val($('#file_ids').val() + ',');
}
$('#file_ids').val($('#file_ids').val() + file.fileID);
});
$('#loading').text('');
}
});
});
</script>

To be honest, I’m not a strong front-ender, so the syntax was written according to jQuery-File-Upload library examples. But basically, it works like this:

  • fileupload() method is attached to input field and takes two important parameters – name=”photos[]” data-url=”/upload”;
  • Those parameters are passed via AJAX request to /upload URL – meaning UploadController and method uploadSubmit();
  • uploadSubmit() physically uploads the file, stores information in the database but doesn’t store product_photos.product_id because we don’t have ID yet. After upload it returns JSON with array of file results;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public function uploadSubmit(Request $request)
{
$photos = [];
foreach ($request->photos as $photo) {
$filename = $photo->store('photos');
$product_photo = ProductPhoto::create([
'filename' => $filename
]);

$photo_object = new \stdClass();
$photo_object->name = str_replace('photos/', '',$photo->getClientOriginalName());
$photo_object->size = round(Storage::size($filename) / 1024, 2);
$photo_object->fileID = $product_photo->id;
$photos[] = $photo_object;
}

return response()->json(array('files' => $photos), 200);

}
  • Those results are shown to the user in the file list (filename and size) and also in the hidden field file_ids which stores values from product_photos.id column;

We can upload more files like this, and our files list will grow bigger and bigger. As soon as we hit the main submit – the data will be posted and UploadController method postProduct() will save the data into products DB table, and also assign new product ID to product_photos entries:

1
2
3
4
5
6
7
public function postProduct(Request $request)
{
$product = Product::create($request->all());
ProductPhoto::whereIn('id', explode(",", $request->file_ids))
->update(['product_id' => $product->id]);
return 'Product saved successfully';
}

https://laraveldaily.com/laravel-ajax-file-upload-blueimp-jquery-library/

Hope is a good thing and maybe the best of things. And no good thing ever dies.

Starting at v5.4.12, Laravel Collections now includes a when method that allows you to perform conditional actions on the items without breaking the chain.

Like all the other Laravel Collection methods this one can have a lot of use cases but one example that comes to mind is being able to filter based on a query string parameter.

To demonstrate that example, let’s pretend we have a list of hosts from the Laravel News Podcast:

1
2
3
4
5
6
$hosts = [
['name' => 'Eric Barnes', 'location' => 'USA', 'is_active' => 0],
['name' => 'Jack Fruh', 'location' => 'USA', 'is_active' => 0],
['name' => 'Jacob Bennett', 'location' => 'USA', 'is_active' => 1],
['name' => 'Michael Dyrynda', 'location' => 'AU', 'is_active' => 1],
];

Previously to filter based on a query string you might do something like this:

1
2
3
4
5
6
7
$inUsa = collect($hosts)->where('location', 'USA');

if (request('retired')) {
$inUsa = $inUsa->filter(function($employee){
return ! $employee['is_active'];
});
}

With the new when method you can now do this all in one Collection chain:

1
2
3
4
5
6
7
$inUsa = collect($hosts)
->where('location', 'USA')
->when(request('retired'), function($collection) {
return $collection->reject(function($employee){
return $employee['is_active'];
});
});

https://laravel-news.com/laravel-collections-when-method

我徒然学会了拒绝热闹,却还未透悟真正的冷清

简介

一个使用虚拟货币进行交易的项目, 通过小金额的参与, 获得大的收益,架构上才用Laravel和Swooole相结合,Socket消息事实推送。

功能点

首页展示

倒计时购买页面

邀请奖励页面

开奖锁定倒计时页面

中奖通知页面

自动充值页面

提现页面

年轻时总以为能遇上许许多多的人。而后你就明白,所谓机缘,其实也不过那么几次。

守护进程(daemon)就是一种长期生存的进程,它不受终端的控制,可以在后台运行。其实我们之前也有了解,比如说nginx,fpm等一般都是作为守护进程在后台提供服务。

熟悉linux的同学可能知道,我们可以利用nohup命令让程序在后台跑。swoole官方也为我们提供了配置选项daemonize,默认不启用守护进程,若要开启守护进程,daemonize设置为true即可。

守护进程有优点,必然也存在缺点。我们启用守护进程后,server内所有的标准输出都会被丢弃,这样的话我们也就无法跟踪进程在运行过程中是否异常之类的错误信息了。为方便起见,swoole为我们提供了另一个配置选项log_file,我们可以指定日志路径,这样swoole在运行时就会把所有的标准输出统统记载到该文件内。

信号

我们了解到,swoole是常驻内存的,若想让修改后的代码生效,就必须Ctrl+C,然后再重启server。对于守护进程化的server呢?了解过kill命令的同学要说了,我直接把它干掉,然后终端下再重启,就可以了。

事实上,对于线上繁忙的server,如果你直接把它干掉了,可能某个进程刚好就只处理了一半的数据,对于天天来回倒腾的你来说,面对错乱的数据你不头疼,DBA也想long死你!

这个时候我们就需要考虑如何平滑重启server的问题了。所谓的平滑重启,也叫“热重启”,就是在不影响用户的情况下重启服务,更新内存中已经加载的php程序代码,从而达到对业务逻辑的更新。

swoole为我们提供了平滑重启机制,我们只需要向swoole_server的主进程发送特定的信号,即可完成对server的重启。

那什么是信号呢?

信号是软件中断,每一个信号都有一个名字。通常,信号的名字都以“SIG”开头,比如我们最熟悉的Ctrl+C就是一个名字叫“SIGINT”的信号,意味着“终端中断”。

平滑重启

在swoole中,我们可以向主进程发送各种不同的信号,主进程根据接收到的信号类型做出不同的处理。比如下面这几个

  • SIGTERM,一种优雅的终止信号,会待进程执行完当前程序之后中断,而不是直接干掉进程
  • SIGUSR1,将平稳的重启所有的Worker进程
  • SIGUSR2,将平稳的重启所有的Task进程

如果我们要实现重启server,只需要向主进程发送SIGUSR1信号就好了。

平滑重启的原理是当主进程收到SIGUSR1信号时,主进程就会向一个子进程发送安全退出的信号,所谓的安全退出的意思是主进程并不会直接把Worker进程杀死,而是等这个子进程处理完手上的工作之后,再让其光荣的“退休”,最后再拉起新的子进程(重新载入新的PHP程序代码)。然后再向其他子进程发送“退休”命令,就这样一个接一个的重启所有的子进程。

我们注意到,平滑重启实际上就是让旧的子进程逐个退出并重新创建新的进程。为了在平滑重启时不影响到用户,这就要求进程中不要保存用户相关的状态信息,即业务进程最好是无状态的,避免由于进程退出导致信息丢失。

在swoole中,重启只能针对Worker进程启动之后载入的文件才有效!什么意思呢,就是说只有在onWorkerStart回调之后加载的文件,重启才有意义。在Worker进程启动之前就已经加载到内存中的文件,如果想让它重新生效,还是只能乖乖的关闭server再重启。

我们看个例子看看到底怎么样向主进程发送SIGUSR1信号以便有效重启Worker进程。

1
2
3
4
5
6
7
8
9
<?php

class Test
{
public function run($data)
{
echo $data;
}
}

在Test::run方法中,我们第一步仅仅是echo输出swoole_server接收到的数据。

当前目录下我们创建一个swoole_server的类NoReload.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

require_once("Test.php");

class NoReload
{
private $_serv;
private $_test;

/**
* init
*/
public function __construct()
{
$this->_serv = new Swoole\Server("127.0.0.1", 9501);
$this->_serv->set([
'worker_num' => 1,
]);
$this->_serv->on('Receive', [$this, 'onReceive']);

$this->_test = new Test;
}
/**
* start server
*/
public function start()
{
$this->_serv->start();
}
public function onReceive($serv, $fd, $fromId, $data)
{
$this->_test->run($data);
}
}

$noReload = new NoReload;
$noReload->start();

特别提醒:我们在初始化swoole_server的时候的写法是命名空间的写法

1
new Swoole\Server

该种风格的写法等同于下划线写法 ,swoole对这两种风格的写法都支持

1
new swoole_server

此外我们看下server的代码逻辑:类定义之前require_once了Test.php,初始化的时候设置了一个Worker进程,注册了NoReload::onReceive方法为swoole_server的onReceive回调,在onReceive回调内接收到的数据传递给了Test::run方法处理。

接下来我们写一个client脚本测试下运行结果

1
2
3
4
5
6
7
8
<?php

$client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_SYNC);
$client->connect('127.0.0.1', 9501) || exit("connect failed. Error: {$client->errCode}\n");

// 向服务端发送数据
$client -> send("Just a test.\n");
$client->close();

客户端的测试代码也很简单,连接server并向server发一个字符串信息

在server不动的情况下我们修改下Test.php,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

class Test
{
public function run($data)
{
// echo $data;
$data = json_decode($data, true);
if (!is_array($data)) {
echo "server receive \$data format error.\n";
return ;
}
var_dump($data);
}
}

原先echo直接输出,现在我们改了下Test的代码,如果接收到的数据经过json_decode处理后不是数组,就返回一段内容并结束,否则打印receive到的数据

如果这个时候我们不对server进行重启,运行client的结果肯定还是一样的

server端新的代码未生效,如果Test.php新的代码生效了,会在server所在终端输出“server receive $data format error.”,这符合我们的认知。

下面我们通过ps命令查看下左侧server的主进程的pid,然后通过kill命令向该进程发送SIGUSR1信号,看看结果如何

结果发现即使向主进程发送了SIGUSR1信号,但是左侧server终端显示的依然是未生效的php代码,这也是对的,因为我们说过新的程序代码只针对在onWorkerStart回调之后才加载进来的php文件才能生效,我们事例中Test.php是在class定义之前就加载进来了,所以肯定不生效。

我们新建一个Reload.php文件,把server的代码修改如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php

class Reload
{
private $_serv;
private $_test;

/**
* init
*/
public function __construct()
{
$this->_serv = new Swoole\Server("127.0.0.1", 9501);
$this->_serv->set([
'worker_num' => 1,
]);
$this->_serv->on('Receive', [$this, 'onReceive']);
$this->_serv->on('WorkerStart', [$this, 'onWorkerStart']);
}
/**
* start server
*/
public function start()
{
$this->_serv->start();
}
public function onWorkerStart($serv, $workerId)
{
require_once("Test.php");
$this->_test = new Test;
}
public function onReceive($serv, $fd, $fromId, $data)
{
$this->_test->run($data);
}
}

$reload = new Reload;
$reload->start();

仔细观察,我们仅仅移除了在类定义之前引入Test.php以及在__construct中new Test的操作。

而是在__construct方法中增加了onWorkerStart回调,并在该回调内引入Test.php并初始化Test类。

Test.php的代码,我们仍然先后用上面的两处代码为例,运行client看下结果

结果我们发现,在给主进程发送SIGUSR1信号之后,Test.php的新代码生效了。这也便实现了热重启的效果。

如此,我们在Test.php中不论如何更新代码,只需要找到主进程的PID,向它发送SIGUSR1信号即可。同理,SIGUSR2信号是只针对Task进程的,后面可以自行测试下。

热重启的效果实现了,现在针对Reload.php的server,让该server进程守护化看看。

1
2
3
4
5
$this->_serv->set([   
'worker_num' => 1,
'daemonize' => true,
'log_file' => __DIR__ . '/server.log',
]);

具体操作

设置进程名

1
2
3
$http->on('start', function ($server) {
swoole_set_process_name('yangzie-test');
});

查看

1
netstat -npl|grep 9501

shell脚本

1
2
3
4
5
echo "loading..."
pid=`pidof yangzie-test`
echo $pid
kill -USR1 $pid
echo "loading success"

https://wiki.swoole.com/wiki/page/p-server/reload.html

有人住高楼,有人在深沟,有人光万丈,有人一身锈。世人万千种,浮云莫去求,斯人若彩虹,遇上方知有。

我们在写代码的过程中应该注意的问题:

  • 开启数量适中的Worker进程和Task进程
  • 守护进程化
  • 配置运行时日志
  • 平滑重启
  • 避免内存泄漏
  • 避免粘包问题

除此之外,跟swoole打交道,我们还应该注意下面这些问题:

  • 为了避免Worker阻塞,避免使用sleep等睡眠函数
  • 不要使用die或者exit函数,即使在你调试的时候
  • 保持良好的代码风格,try/catch捕获异常
  • 如果Worker进程无法预料会发生异常退出,虽然Manager进程会重新拉起新的Worker进程,但是我们可以通过register_shutdown_function方法在进程退出前“善后”

邮件案例

首先发送邮件,我们借助第三方类库 swiftmailer。有些框架可能集成了swiftmailer,比如yii2,本来准备在yii2的基础之上来讲,考虑部分人可能对这个框架不熟悉,我们这里直接根据swiftmailer代码操作,框架中一样可以使用,无任何影响。

我们执行下面的命令,把swiftmailer下载到本地,下载好之后swiftmailer会被下载到一个叫vendor文件夹的目录里面

1
composer require "swiftmailer/swiftmailer:^6.0"

然后我们封装一个简单的邮件类Mailer.php,同vendor目录同级,用于发送邮件,该类后期可自行完善,比如增加批量发送邮件或者增加发送模版邮件等操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
require_once __DIR__ . '/vendor/autoload.php';

class Mailer
{
public $transport;
public $mailer;
/**
* 发送邮件类 参数 $data 需要三个必填项 包括 邮件主题`$data['subject']`、接收邮件的人`$data['to']`和邮件内容 `$data['content']`
* @param Array $data
* @return bool $result 发送成功 or 失败
*/
public function send($data)
{
$this->transport = (new Swift_SmtpTransport('smtp.qq.com', 25))
->setEncryption('tls')
->setUsername('bailangzhan@qq.com')
->setPassword('xxxxxx');
$this->mailer = new Swift_Mailer($this->transport);

$message = (new Swift_Message($data['subject']))
->setFrom(array('bailangzhan@qq.com' => '白狼栈'))
->setTo(array($data['to']))
->setBody($data['content']);

$result = $this->mailer->send($message);

// 释放
$this->destroy();
return $result;
}
public function destroy()
{
$this->transport = null;
$this->mailer = null;
}
}

在这段代码中,你需要修改的地方包括 Host、Post、Encryption、Username、Password和From。

Mailer类简单的封装好之后,我们写几行代码测试下你的邮件类是否可以正确的使用

1
2
3
4
5
6
7
8
require_once __DIR__ . "/task/Mailer.php";
$data = [
'to' => '422744***@qq.com',
'subject' => 'just a test',
'content' => 'This is just a test.',
];
$mailer = new Mailer;
$mailer->send($data);

to是要发送给谁,subject邮件标题,content邮件内容。

如果不可以正常发送,请检查swiftmailer相关类正确引入并且保证Mailer类的配置可用。

邮件类准备好之后,我们正式开始写swoole server,主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<?php
class TaskServer
{
private $_serv;
private $_run;
/**
* init
*/
public function __construct()
{
$this->_serv = new Swoole\Server("127.0.0.1", 9501);
$this->_serv->set([
'worker_num' => 2,
'daemonize' => false,
'log_file' => __DIR__ . '/server.log',
'task_worker_num' => 2,
'max_request' => 5000,
'task_max_request' => 5000,
'open_eof_check' => true, //打开EOF检测
'package_eof' => "\r\n", //设置EOF
'open_eof_split' => true, // 自动分包
]);
$this->_serv->on('Connect', [$this, 'onConnect']);
$this->_serv->on('Receive', [$this, 'onReceive']);
$this->_serv->on('WorkerStart', [$this, 'onWorkerStart']);
$this->_serv->on('Task', [$this, 'onTask']);
$this->_serv->on('Finish', [$this, 'onFinish']);
$this->_serv->on('Close', [$this, 'onClose']);
}
public function onConnect($serv, $fd, $fromId)
{
}
public function onWorkerStart($serv, $workerId)
{
require_once __DIR__ . "/TaskRun.php";
$this->_run = new TaskRun;
}
public function onReceive($serv, $fd, $fromId, $data)
{
$data = $this->unpack($data);
$this->_run->receive($serv, $fd, $fromId, $data);
// 投递一个任务到task进程中
if (!empty($data['event'])) {
$serv->task(array_merge($data , ['fd' => $fd]));
}
}
public function onTask($serv, $taskId, $fromId, $data)
{
$this->_run->task($serv, $taskId, $fromId, $data);
}
public function onFinish($serv, $taskId, $data)
{
$this->_run->finish($serv, $taskId, $data);
}
public function onClose($serv, $fd, $fromId)
{
}
/**
* 对数据包单独处理,数据包经过`json_decode`处理之后,只能是数组
* @param $data
* @return bool|mixed
*/
public function unpack($data)
{
$data = str_replace("\r\n", '', $data);
if (!$data) {
return false;
}
$data = json_decode($data, true);
if (!$data || !is_array($data)) {
return false;
}
return $data;
}
public function start()
{
$this->_serv->start();
}
}
$reload = new TaskServer;
$reload->start();

简单分析下:

  • 在onWorkerStart回调内,我们引入了实际处理业务逻辑的类TaskRun.php,为什么这么说呢?因为我们在onReceive\onTask\onFinish回调内均把数据交给了TaskRun对象去处理了
  • 我们约定,每个数据包都必须带有EOF标记\r\n,在server端为了更好的处理数据,onReceive回调内我们把数据包丢给了unpack方法处理,该方法的目的就是把数据包的EOF标记去掉,还原真实的数据包。我们还约定,server收到的数据包经过unpack处理之后只能是数组,非数组在unpack中就被直接处理掉了。
  • onReceive回调内,我们看到,只有数据包含有event项才会被投递给Task进程,这样做的原因是Task进程可能要处理各种任务,增加event项是为了表明投递过来的任务是要做什么的。

我们看TaskRun的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
require_once ('./TaskClient.php');
require_once ('./Mailer.php');
class TaskRun
{
public function receive($serv, $fd, $fromId, $data)
{
}
public function task($serv, $taskId, $fromId, $data)
{
try {
switch ($data['event']) {
case TaskClient::EVENT_TYPE_SEND_MAIL:
$mailer = new Mailer;
$result = $mailer->send($data);
break;
default:
break;
}
return $result;
} catch (\Exception $e) {
throw new \Exception('task exception :' . $e->getMessage());
}
}
public function finish($serv, $taskId, $data)
{
return true;
}
}

目前,我们主要就一个业务,“发送邮件”,所以TaskRun类的实现现在看来非常简单。

因为发邮件是一件比较耗时的任务,所以我们这里完善的是task回调。我们根据投递给Task进程的数据类型,判断投递过来的数据是要做什么。比如我们这里有一项event,等于TaskClient::EVENT_TYPE_SEND_MAIL,这一项就是发送邮件的标识,如果要投递的任务的event项等于TaskClient::EVENT_TYPE_SEND_MAIL,就表明这个任务是邮件任务,程序上就可以通过switch去处理邮件了。

TaskClient是什么呢?这是一个封装好的客户端处理类,我们来看下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
class TaskClient
{
private $client;
const EVENT_TYPE_SEND_MAIL = 'send-mail';
public function __construct ()
{
$this->client = new Swoole\Client(SWOOLE_SOCK_TCP);
if (!$this->client->connect('127.0.0.1', 9501)) {
$msg = 'swoole client connect failed.';
throw new \Exception("Error: {$msg}.");
}
}
/**
* @param $data Array
* send data
*/
public function sendData ($data)
{
$data = $this->togetherDataByEof($data);
$this->client->send($data);
}
/**
* 数据末尾拼接EOF标记
* @param Array $data 要处理的数据
* @return String json_encode($data) . EOF
*/
public function togetherDataByEof($data)
{
if (!is_array($data)) {
return false;
}
return json_encode($data) . "\r\n";
}
}

人不知道要守住多少秘密才能安度一生。

什么是粘包问题,为什么我们要讲这个看起来比较奇怪的问题呢?
不着急解释,我们先看一个例子
创建一个server,server端代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php

class TcpBufferServer
{
private $_serv;

/**
* init
*/
public function __construct()
{
$this->_serv = new Swoole\Server("127.0.0.1", 9501);
$this->_serv->set([
'worker_num' => 1,
]);
$this->_serv->on('Receive', [$this, 'onReceive']);
}
public function onReceive($serv, $fd, $fromId, $data)
{
echo "Server received data: {$data}" . PHP_EOL;
}
/**
* start server
*/
public function start()
{
$this->_serv->start();
}
}

$reload = new TcpBufferServer;
$reload->start();

server的代码很简单,仅仅是在收到客户端代码后,标准输出一句话而已,client的代码需要注意了,我们写了一个for循环,连续向server send三条信息,代码如下

1
2
3
4
5
6
7
8
9
10
<?php

$client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_SYNC);
$client->connect('127.0.0.1', 9501) || exit("connect failed. Error: {$client->errCode}\n");

// 向服务端发送数据
for ($i = 0; $i < 3; $i++) {
$client->send("Just a test.\n");
}
$client->close();

在未运行测试的情况下,我们期望server所在终端输出的结果应该是这样的

1
2
3
Server received data: Just a test.
Server received data: Just a test.
Server received data: Just a test.

注意哦,我们期望的结果是server被回调了3次,才有上述期望的结果值
实际运行的结果呢?

上图左边是server输出的信息。
我们看到,左侧显示的结果是server一次性输出的结果,按理论来说,client发起了3次请求,server应该跟我们期望的结果一致,会执行3次呀,这怎么回事呢?
这个问题,便是我们今天要说的粘包问题。
为了说清楚这个问题,我们先来看下client/server之间数据传递的过程

  • 客户端->发送数据
  • 服务端->接收数据

通常我们直觉性的认为,客户端直接向网络中传输数据,对端从网络中读取数据,但是这是不正确的。
socket有缓冲区buffer的概念,每个TCP socket在内核中都有一个发送缓冲区和一个接收缓冲区。客户端send操作仅仅是把数据拷贝到buffer中,也就是说send完成了,数据并不代表已经发送到服务端了,之后才由TCP协议从buffer中发送到服务端。此时服务端的接收缓冲区被TCP缓存网络上来的数据,而后server才从buffer中读取数据。
所以,在onReceive中我们拿到的数据并没有办法保证数据包的完整性,swoole_server可能会同时收到多个请求包,也可能只收到一个请求包的一部分数据。
这就是一个大问题呀,如此TCP协议不行呀,这货虽然能保证我们能正确的接收到数据但是数据不对呀,这麻烦不容小觑。
既然是个问题,那我们自然也就有解决问题的方法,不然我下面说啥呢,对吧。

swoole给我们提供了两种解决方案:

EOF结束协议

EOF,end of file,意思是我们在每一个数据包的结尾加一个eof标记,表示这就是一个完整的数据包,但是如果你的数据本身含有EOF标记,那就会造成收到的数据包不完整,所以开启EOF支持后,应避免数据中含有EOF标记。

在swoole_server中,我们可以配置open_eof_check为true,打开EOF检测,配置package_eof来指定EOF标记。

swoole_server收到一个数据包时,会检测数据包的结尾是否是我们设置的EOF标记,如果不是就会一直拼接数据包,直到超出buffer或者超时才会终止,一旦认定是一个完整的数据包,就会投递给Worker进程,这时候我们才可以在回调内处理数据。

这样server就能保证接收到一个完整的数据包了?不能保证,这样只能保证server能收到一个或者多个完整的数据包。

为啥是多个呢?

我们说了开启EOF检测,即open_eof_check设置为true,server只会检测数据包的末尾是否有EOF标记,如果向我们开篇的案例连发3个EOF的数据,server可能还是会一次性收到,这样我们只能在回调内对数据包进行拆分处理。

我们拿开篇的案例为例

server开启eof检测并指定eof标记是\r\n,代码如下

1
2
3
4
5
$this->_serv->set([ 
'worker_num' => 1,
'open_eof_check' => true, //打开EOF检测
'package_eof' => "\r\n", //设置EOF
]);

客户端设置发送的数据末尾是\r\n符号,代码如下

1
2
3
for ($i = 0; $i < 3; $i++) { 
$client->send("Just a test.\r\n");
}

按照我们刚才的分析,server的效果可能会一次性收到多个完整的包,我们运行看看结果

因此我们还需要在onReceive回调内对收到的数据进行拆分处理

1
2
3
4
5
6
7
8
9
10
11
12
13
public function onReceive($serv, $fd, $fromId, $data)
{
// echo "Server received data: {$data}" . PHP_EOL;

$datas = explode("\r\n", $data);
foreach ($datas as $data)
{
if(!$data)
continue;

echo "Server received data: {$data}" . PHP_EOL;
}
}

此时我们再看下运行结果

自行分包的效果便实现了,考虑到自行分包稍微麻烦,swoole提供了open_eof_split配置参数,启用该参数后,server会从左到右对数据进行逐字节对比,查找数据中的EOF标记进行分包,效果跟我们刚刚自行拆包是一样的,性能较差。
在案例的基础上我们看看open_eof_split配置

1
2
3
4
5
6
$this->_serv->set([
'worker_num' => 1,
'open_eof_check' => true, //打开EOF检测
'package_eof' => "\r\n", //设置EOF
'open_eof_split' => true,
]);

onReceive的回调,我们不需要自行拆包

1
2
3
4
public function onReceive($serv, $fd, $fromId, $data)
{
echo "Server received data: {$data}" . PHP_EOL;
}

client的测试代码使用\r\n(同server端package_eof标记一致),我们看下运行效果

EOF标记解决粘包就说这么多,下面我们再看看另一种解决方案

固定包头+包体协议

下面我们要说的,对于部分同学可能有点难度,对于不理解的,建议多看多操作多问多查,不躲避不畏惧,这样才能有所提高。
固定包头是一种非常通用的协议,它的含义就是在你要发送的数据包的前面,添加一段信息,这段信息了包含了你要发送的数据包的长度,长度一般是2个或者4个字节的整数。
在这种协议下,我们的数据包的组成就是包头+包体。其中包头就是包体长度的二进制形式。比如我们本来想向服务端发送一段数据 “Just a test.” 共12个字符,现在我们要发送的数据就应该是这样的

1
pack('N', strlen("Just a test.")) . "Just a test."

其中php的pack函数是把数据打包成二进制字符串。
为什么这样就能保证Worker进程收到的是一个完整的数据包呢?我来解释一下:
当server收到一个数据包(可能是多个完整的数据包)之后,会先解出包头指定的数据长度,然后按照这个长度取出后面的数据,如果一次性收到多个数据包,依次循环,如此就能保证Worker进程可以一次性收到一个完整的数据包。
估计好多人都看蒙了,这都是神马玩意?我们以案例来分析
server代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php

class ServerPack
{
private $_serv;

/**
* init
*/
public function __construct()
{
$this->_serv = new Swoole\Server("127.0.0.1", 9501);
$this->_serv->set([
'worker_num' => 1,
'open_length_check' => true, // 开启协议解析
'package_length_type' => 'N', // 长度字段的类型
'package_length_offset' => 0, //第几个字节是包长度的值
'package_body_offset' => 4, //第几个字节开始计算长度
'package_max_length' => 81920, //协议最大长度
]);
$this->_serv->on('Receive', [$this, 'onReceive']);
}
public function onReceive($serv, $fd, $fromId, $data)
{
$info = unpack('N', $data);
$len = $info[1];
$body = substr($data, - $len);
echo "server received data: {$body}\n";
}
/**
* start server
*/
public function start()
{
$this->_serv->start();
}
}

$reload = new ServerPack;
$reload->start();

客户端的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

$client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_SYNC);
$client->connect('127.0.0.1', 9501) || exit("connect failed. Error: {$client->errCode}\n");

// 向服务端发送数据
for ($i = 0; $i < 3; $i++) {
$data = "Just a test.";
$data = pack('N', strlen($data)) . $data;
$client->send($data);
}

$client->close();

运行的结果

结果没错,是我们期望的结果。

我们来分析下这是为什么

1、首先,在server端我们配置了open_length_check,该参数表明我们要开启固定包头协议解析

2、package_length_type配置,表明包头长度的类型,这个类型跟客户端使用pack打包包头的类型一致,一般设置为N或者n,N表示4个字节,n表示2个字节

3、我们看下客户端的代码 pack(‘N’, strlen($data)) . $data,这句话就是包头+包体的意思,包头是pack函数打包的二进制数据,内容便是真实数据的长度 strlen($data)。

在内存中,整数一般占用4个字节,所以我们看到,在这段数据中0-4字节表示的是包头,剩余的就是真实的数据。但是server不知道呀,怎么告诉server这一事实呢?

看配置package_length_offset和package_body_offset,前者就是告诉server,从第几个字节开始是长度,后者就是从第几个字节开始计算长度。

4、既然如此,我们就可以在onReceive回调对数据解包,然后从包头中取出包体长度,再从接收到的数据中截取真正的包体。

1
2
3
4
$info = unpack('N', $data);
$len = $info[1];
$body = substr($data, - $len);
echo "server received data: {$body}\n";